先记住这个答案
every 的意思是「所有元素都满足条件」,换个说法就是「找不到一个不满足的」。空数组里没有任何元素,当然找不到反例,所以返回 true。再看 some,它的意思是「至少有一个元素满足条件」,空数组里一个元素都没有,找不到例子,所以返回 false。这两种情况下回调一次都不会被调用。业务上如果要求「至少有一条,并且全部合格」,必须把非空判断单独写出来。
every找反例,空数组没有反例,返回truesome找例子,空数组没有例子,返回false- 两者都会短路,空位直接跳过不调用回调
- 业务上要求「至少一条」,要单独判断
length
回调被调用了几次
用一个计数器看清楚 every 和 some 的执行过程。
let calls = 0
const check = (x) => { calls++; return x > 0 }
console.log([].every(check), [].some(check), calls) // 空数组:回调没被调用
console.log([1, -1, 2].every(check), calls) // 遇到 -1 就停了
console.log([-1, 2, 3].some(check), calls) // 遇到 2 就停了查看输出与解释
true false 0
false 2
true 4第一行,两个方法直接给出默认结果,calls 还是 0。第二行,every 检查到 -1 时已经找到反例,结果确定是 false,后面的 2 就不看了,所以只调用了两次。第三行,some 检查到 2 时已经找到例子,也停了。
这就是短路:结果一旦确定就不再往下走。所以在数据量大、检查代价高的场景里,every 和 some 比先 filter 再比较长度要省。
为什么默认值是这两个
可以把 every 想成用 && 把每一项的结果连起来:p(a) && p(b) && p(c)。要让「一项都没有」的情况也能接进这个链条,起点必须是 true,因为 true && x 等于 x,不影响结果。同理,some 是 p(a) || p(b) || p(c),起点必须是 false。
这和数学里「空求和等于 0,空求积等于 1」是一个道理,起点都是那个不会改变结果的值。两个方法还能互相转换:arr.every(p) 等于 !arr.some((x) => !p(x)),「全部满足」就是「不存在不满足的」。空数组代进去,some 返回 false,取反正好是 true,逻辑是自洽的。
const items = [] // 购物车是空的
const canSubmit = items.every((i) => i.checked)
const canSubmit2 = items.length > 0 && items.every((i) => i.checked)
console.log(canSubmit, canSubmit2)
console.log([, ,].every(() => false)) // 全是空位,回调一次都没调用查看输出与解释
true false
true「所有商品都勾选了才能提交」直接写成 every,空购物车会被判成可以提交。正确写法要把「至少一件」和「每件都勾选」分开表达。第二段说明空位也会被跳过:length 是 2,但两个位置都是空位,回调没有执行,结果和空数组一样。所以 length > 0 也不能完全保证有真实数据,数据来自不可信来源时还要检查元素本身。
容易答错的地方
- 以为空数组
every返回false才合理 - 直觉上「什么都没有」不该算「全部满足」,但这是逻辑上的空真命题,和「这个空房间里所有人都戴了帽子」一样,没法举出反例就是真。规范按这个逻辑定义,所有语言的同类函数也都是这样,比如 Python 的
all([])也是True。 - 在
every里写async回调 items.every(async (i) => await isValid(i))永远返回true。每次回调返回的都是一个 Promise 对象,对象是真值,every不会等它兑现。要做异步校验,先const results = await Promise.all(items.map(isValid)),再对results调用every。
面试官还会怎么问?
手写一个 every,初始值怎么定?
初始结果设成 true,遍历时遇到回调返回假值就立刻 return false,循环结束后 return true。空数组时循环不执行,直接返回初始的 true。实现时还要用 i in arr 跳过空位,和原生行为保持一致。
every 和 find、findIndex 在空位上的处理一样吗?
不一样。every、some、forEach、map、filter 会跳过空位;find 和 findIndex 是后来加入的方法,会把空位当成 undefined 传给回调。所以 [, 1].findIndex((x) => x === undefined) 返回 0,下标 0 的空位被命中了。
遍历过程中数组被修改了会怎样?
every 开始时就记住了 length,中途 push 的新元素不会被访问。已经在范围内但还没访问到的元素如果被修改,读到的是新值;如果被删除,就会被当作空位跳过。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。