前言
人生总是在意外之中. 情况大概是这样的. 前两天版本上线以后, 无意中发现了一个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, 则停止循环, 整体返回falseif(testResult === false){return false}}k++}// 数组元素循环完毕, 回调函数都没有返回false, 则表示数组每一项否符合要求// 最终返回truereturn 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
, 这个留给你自己探讨