前言
人生总是在意外之中. 情况大概是这样的. 前两天版本上线以后, 无意中发现了一个bug
, 虽然不是很大, 为了不让用户使用时感觉到问题. 还是对着一个小小的bug
进行了修复, 并重新在上线一次, 虽然问题不大, 但带来的时间成本还是存在的. 以及上线后用户体验并不是很好.
问题产生的原因就在于JavaScript
数组遍历方法中对于空数组返回值的问题. 空数组遍历的知识点, 在学习的过程中, 相信你肯定也接触过, 学习过. 但在使用时往往会忽略这一点.
我们以every
为例
1. every 基本使用
对于every
遍历方法, 这里就不过多阐述了. 主要就是遍历数组中每个元素, 执行回调函数, 当所有的元素都返回true
时, 结果是true
, 只要有一次遍历时, 回调函数返回false
结果就是false
示例:
let arr = [40,50,60,10,20,30]
// 判断数组中所有的元素是否都大于5
let bol = arr.every((item) => item > 5)
console.log('bol', bol)
// 输出结果: bol true
// 判断数组中所有的元素是否都大于
let bol2 = arr.every((item) => item > 10)
console.log('bol2', bol2)
// 输出结果: bol2 false
在这个示例中, 第一次调用every
时, 会遍历所有的数组元素, 因为每一个元素都大于5, 所以回调函数会执行6次, 每次都返回true
, every
遍历方法最终返回true
, 表示数组中每个元素都符合要求
第二次遍历时, 只会遍历4次, 因为在遍历到10 时, 回调函数返回false
, 此时数组后面的元素就不需要再遍历了. 该false
已经确定了最终的结果, false
表示数组中包含不符号要求的元素.
every
遍历方法并不需要关心具体是第几项元素不符合要求. 该方法的作用就是判断数组中是否是每一项都符合要求
2. every 空数组的问题
我们先说一下最终的结果, 空数组使用every
时, 每次结果都返回true
示例:
let arr = []
// 固定返回true
let bol = arr.every((item) => true)
console.log('bol', bol)
// 输出结果: bol true
// 回调函数固定返回false
let bol2 = arr.every((item) => {
console.log('every')
return false
})
console.log('bol2', bol2);
// bol2 true
示例中, 我们对于空数组使用every
遍历方法, 无论回调函数返回的是true
,还是false
最终的结果都是true
我们在回调函数中添加console.log("every")
记录回调函数是否执行. 从运行结果来看, 会调函数并没有执行. 空数组中没有元素, 并不会执行回调函数, 也就意味这回调函数中返回的是什么值都毫无意义.
every
遍历方法最终的结果true
显然是一个默认值. 可以理解为调用every
时, 默认返回值就是true
, 然后遍历元素,执行回调函数, 只要有一次回调函数返回的false
, 则作为最终结果返回false
并结束遍历.
3. 规范描述
根据ECMA-262 官方描述, every
方法的逻辑如下
这里对于最终返回值, 我将其框选出来. 通过官方描述, 最终的结果有两种情况, 其一就是默认返回true
, 其二是根据回调函数执行的结果返回false
,
这里我们根据表述, 自定义一个函数模拟every
方法
示例
// 参数接受一个回调函数
Array.prototype.myEvery = (callbackfn, thisArg) => {
// 获取this, 通过数组调用该方法, this 即为数组
const O = this;
// 获取数组长度
const len = O.length;
// 确认参数callback 是一个函数. 否则报错
if(typeof callbackfn !== "function"){
throw new TypeError(typeof callbackfn + "is not a function")
}
// 遍历数组
let k = 0;
while(k < len){
// 转为字符串
const Pk = String(key)
// 判断下标是否为数组本身的属性
const kPresent = O.hasOwnProperty(Pk);
if(kPresent){
// 获取数组元素
const kValue = O[PK]
// 调用回调函数, 获取回调函数的结果
const testResult = Boolean( callbackfn.call(this.Arg, kValue, k, O))
// 如果回调函数返回false, 则停止循环, 整体返回false
if(testResult === false){
return false
}
}
k++
}
// 数组元素循环完毕, 回调函数都没有返回false, 则表示数组每一项否符合要求
// 最终返回true
return true
}
从代码中可以看出,every ()
默认返回为 true,并且只有在回调函数执行返回 false
时才返回 false
。如果数组中没有元素,那么就没有机会执行回调函数,因此方法就没有办法返回 false
,只会返回默认值true
。
4. 场景描述
在工作中发生问题场景是这样的, 在使用vue
开发, 父组件给子组件传参. 期望传入的 参数在子组件的本身数据中都包含, 则不执行后续逻辑, 如果传入的数据, 在子组件数据中有不存的项, 则更新子组件数据.
这里我将复杂业务逻辑简化为JavaScript
方法的调用.
示例:
let cacheArray = [10,20,30];
function update (arr) {
// 判断传入的数组每一项存在于cacheArray 中
const result = arr.every((item) => cacheArray.includes(item))
// 条件为true, 则表示传入数组中的数据都存在, 则不执行后续逻辑,
// false, 更新cacheArray 数据, 并执行后续逻辑
if(!result){
cacheArray = [...arr]
console.log('cacheArray', cacheArray)
}
}
这个示例代码在表面上看没有任何问题, 但如果你想清空cacheArray
数组. 你会发现调用update
方法做不到,
当你调用update
方法,并传入一个[]
时, 空数组调用every
的结果始终是true
, 所以后面取反的结果始终为false
, 根本执行更新cacheArray
数组的代码.
发现问题点, 解决方案就很简单了, 修改一下判断条件, 当参数为空数组时. length
必然是0
, 通过判断是空数组, 根据逻辑运算符的||
的短路算法规则, 不需再去判断!result
修改判断条件
if(!arr.length || !result){
cacheArray = [...arr]
console.log('cacheArray', cacheArray)
}
5. 总结
这就是空数组给功能带来的问题. 所以有的时候, 真的不是我们学习了所有知识, 我们就能做到万无一失. 在工作中存在很多复杂的逻辑, 一个疏忽, 或没有考虑细致都会带来问题. 不是不会, 而是所有业务逻辑交织在一起. 难免带来一些逻辑上的遗漏
对于some
方法也会有相同的问题, 对于空数组会始终返回false
, 这个留给你自己探讨