深入理解 JavaScriptArray.prototype.every():签名、thisArg、短路求值与稀疏数组行为
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
Array.prototype.every()是 JavaScript 数组迭代方法中用于“全量校验”的核心工具:它通过回调函数测试数组中每一个元素是否都满足给定条件,只要有一个元素不满足就立即返回false,全部满足才返回true。本篇文章以 Refine 开源仓库(documentation/blog/2025-01-17-js-every.md)中的同名教程为主体,完整展开方法签名、回调参数、thisArg绑定、短路行为、稀疏/空数组语义等关键细节,并结合 packages 目录中的真实源码使用场景,帮助你在数据校验、权限判断、类型检查等场景中正确、可靠地使用every()。
说明:本文是 Refine JavaScript 迭代方法系列(JavaScript Iteration Methods Series)的第三篇,与 2024-11-04-js-some.md(
some())互为对照。
Array.prototype.every是什么
Array.prototype.every是 JavaScript 的一个数组迭代方法,用于检查数组中的每一个元素是否都满足给定条件。方法被调用在一个数组上,条件通过一个回调函数callbackFn进行检查,并可选择性地传入一个thisArg对象,将该对象注入到回调函数的执行上下文中。
方法签名的两个参数中,第一个参数callbackFn是必选的,第二个参数thisArg是可选的:
// Method signature every(callbackFn); every(callbackFn, thisArg);回调函数的细节
every()的回调函数callbackFn接收三个参数:
element:当前遍历到的元素,必选;index:当前元素的索引,可选;array:正在被遍历的调用方数组,可选。
// Method signature every(function(element){...}); every(function(element, index){...}); every(function(element, index, array){...});every()的工作原理
every()会一直遍历到数组末尾,以测试所有元素是否都满足回调函数callbackFn中指定的条件。它会对数组中的每一项尝试执行一次callbackFn;一旦发现某个元素求值为 falsy,就立即返回布尔值false。否则,它会继续遍历到数组末尾,若所有元素都为 truthy 则返回true:
const numbers = [1, 2, 3, 4, 5]; const numbersDoubled = [2, 4, 6, 8, 10]; const even = (element) => element % 2 === 0; const areAllEven = numbers.every(even); const areAllDoubledEven = numbersDoubled.every(even); console.log(areAllEven); // false console.log(areAllDoubledEven); // true在上面的代码中,even是我们传给every()的回调函数。显然,numbers中至少有一个奇数,因此并非所有元素都通过测试,areAllEven得到false。而numbersDoubled中的所有元素都是偶数,所以areAllDoubledEven得到true。
这种“遇假即停”的特性在英文中称为short-circuiting(短路求值),它使得every()在大型数组上的最坏情况(全部满足条件)才需要完整遍历,一旦提前命中不满足项即可退出,从而节省不必要的迭代开销。
比较表:every与some
| Feature | everyMethod | someMethod |
|---|---|---|
| Purpose | Tests ifallelements pass a condition | Tests ifat least oneelement passes a condition |
| Returns | trueif all elements pass the test | trueif any element passes the test |
| Stops Checking | At the first failing element | At the first passing element |
| Empty Arrays | Always returnstrue | Always returnsfalse |
何时使用 JS Arrayevery方法
every()适用于需要验证所有元素(无一例外)都满足回调函数中测试条件的各类场景。在所有情况下,回调函数的第一个参数(element)都是必须传入的;测试的复杂度决定了是否需要访问第二个参数(index)、第三个参数(array),或者通过thisArg对象传入额外参数。
典型的使用场景包括:
- 简单的线性条件:如上面的
even函数那样,用简单条件测试元素。 - 多个线性条件:更常见的是使用同时涉及
element与index的复杂测试。 - 带额外参数的复杂条件:通过
thisArg属性把额外参数注入回调执行上下文,这是Array.prototype.every的典型用法。 - 涉及嵌套对象的条件:对深层嵌套对象构成的数组使用
every()也很常见。
配合thisArg参数使用
我们可以把thisArg对象传给every(),并将其加入回调函数的执行上下文。先对回调做一些改造:与其只检查偶数,不如将回调泛化为“检查元素是否可被某个数整除”。
function divisible(element, divisor) { return element % divisor === 0; }但问题在于:我们不能把divisor作为divisible()的第二个参数传入——因为回调函数的第二、三个参数已经被index和array占据,若再引入第四个参数会让签名变得拥挤不堪。
解决之道是把divisor作为thisArg对象(即every()的第二个参数)的一个属性传入,然后在回调内部通过this访问它:
const numbers = [1, 2, 3, 4, 5]; const numbersDoubled = [2, 4, 6, 8, 10]; function divisible(element) { return element % this?.divisor === 0; } const areAllEven = numbers.every(divisible, { divisor: 2 }); const areAllDoubledEven = numbersDoubled.every(divisible, { divisor: 2 }); console.log(areAllEven); // false console.log(areAllDoubledEven); // true这里我们把thisArg对象设置为{ divisor: 2 },从而检查元素是否为偶数。
得益于thisArg,我们还能轻易复用它来尝试其他除数,例如检查数组中是否存在能被 3 或 7 整除的数:
const areAllDivisibleByThree = numbers.every(divisible, { divisor: 3 }); const areAllDivisibleBySeven = numbers.every(divisible, { divisor: 7 }); console.log(areAllDivisibleByThree); // false console.log(areAllDivisibleBySeven); // falseevery(callback, thisArg)与箭头函数不兼容
回顾第一个例子中的even()回调,我们把它定义为箭头函数,结果正常工作。其扩展版本divisible()用的是具名函数声明语法,也正常工作。
但如果把divisible()声明为箭头函数,问题就来了:
const numbers = [1, 2, 3, 4, 5]; const numbersDoubled = [2, 4, 6, 8, 10]; const divisible = (element) => element % this?.divisor === 0; const areAllDoubledEven = numbersDoubled.every(divisible, { divisor: 2 }); const areAllDoubledDivisibleByThree = numbersDoubled.every(divisible, { divisor: 3, }); const areAllDoubledDivisibleBySeven = numbersDoubled.every(divisible, { divisor: 7, }); console.log(areAllDoubledEven); // false console.log(areAllDoubledDivisibleByThree); // false console.log(areAllDoubledDivisibleBySeven); // false全部返回false——尽管我们知道areAllDoubledEven本应为true,其余两个本应为false。
如果用一个修改过的divisible()把this打印到控制台,我们会发现在严格模式下this是undefined:
// strict mode const numbers = [1, 2, 3, 4, 5]; const numbersDoubled = [2, 4, 6, 8, 10]; const divisible = (element) => { console.log(this); return element % this?.divisor === 0; }; const areAllDoubledEven = numbersDoubled.every(divisible, { divisor: 2 }); console.log(areAllDoubledEven); // undefined // false现在,如果我们在divisible()的词法环境中引入一个this.divisor属性,控制台就会打印出它的值:
const numbers = [1, 2, 3, 4, 5]; const numbersDoubled = [2, 4, 6, 8, 10]; this.divisor = "Hi"; const divisible = (element) => { console.log(this); return element % this.divisor === 0; }; const areAllDoubledEven = numbersDoubled.every(divisible, { divisor: 2 }); console.log(areAllDoubledEven); // { divisor: 'Hi' } // false这里可以清楚地看到,{ divisor: 'Hi' }来自divisible的闭包。问题的根源在于:因为箭头语法,divisible()的this被绑定到了它的词法环境——在引入this.divisor = 'Hi';之前是undefined,之后变成了{ divisor: 'Hi' }。换句话说,{ divisor: 2 }并没有被传达到divisible的this。
所以结论是:当callbackFn使用箭头语法定义时,every()的thisArg不会按预期工作。
every(callback, thisArg)与非箭头函数兼容
但正如前面所见,使用具名函数声明定义的回调可以正常工作:
function divisible(element) { return element % this?.divisor === 0; } const areAllDoubledEven = numbersDoubled.every(divisible, { divisor: 2 }); console.log(areAllDoubledEven); // true匿名函数表达式同样可以:
const divisible = function (element) { return element % this?.divisor === 0; }; const areAllDoubledEven = numbersDoubled.every(divisible, { divisor: 2 }); console.log(areAllDoubledEven); // true也就是说:只要回调需要在this上读取thisArg注入的数据,就应该使用普通函数(具名声明或函数表达式),而不是箭头函数。
在回调中修改调用方数组
Array.prototype.every在第一次调用回调函数之前就确定了要处理的元素范围。如果某个元素在遍历之后被修改,回调函数会忽略该修改——回调只尊重它被访问时刻该元素已有的值。
every()本身并不会修改调用方数组,但调用方数组会以第三个参数array的形式暴露给回调函数。这意味着,当确有需要时,我们可以从回调内部故意修改调用方数组:
function divisible(element, index, array) { array[0] = 7; array[4] = 7; console.log(array); return element % this?.divisor === 0; }在这种场景下,如果一个尚未访问的元素被提前修改,回调函数(这里即divisible)在访问它时会发现新值并处理新值;而已经遍历过的元素的修改则会被忽略:
const numbers = [1, 2, 3, 4, 5]; const numbersDoubled = [2, 4, 6, 8, 10]; const divisible = function (element, index, array) { array[0] = 7; array[4] = 7; console.log(array); return element % this?.divisor === 0; }; const areAllDoubledEven = numbersDoubled.every(divisible, { divisor: 2 }); console.log(areAllDoubledEven); console.log(numbersDoubled); /* [ 7, 4, 6, 8, 7 ] [ 7, 4, 6, 8, 7 ] [ 7, 4, 6, 8, 7 ] [ 7, 4, 6, 8, 7 ] [ 7, 4, 6, 8, 7 ] false [ 7, 4, 6, 8, 7 ] */上面的控制台输出中,由于divisible()内部放置了console.log(array);,numbersDoubled数组被打印了五次。
可以看到,numbersDoubled在divisible()第一次调用时就发生了两次变更。第一次变更发生在numbersDoubled[0]处——即该位置被访问之后,它自身的值被改成了7。因此,尽管7不能被除数2整除,every()并没有在索引0处立即返回false,而是在下一次访问到已被提前修改的numbersDoubled[4](值同样为7)时才返回false。
这说明:回调函数处理的是元素在遍历时刻被找到时的值,而对“位于当前索引处或之后”的修改不予理会。在实践中,除非确有必要,应尽量避免在every()回调中修改调用方数组,因为这种依赖“访问时机”的行为很容易引入难以排查的 bug。
使用 JS Arrayevery的微妙之处
与稀疏数组(Sparse Arrays)配合使用
现在考虑调用方数组中含有空槽(empty slot)的情况。给numbersDoubled加上两个空项,并移除divisible中的修改逻辑:
const numbers = [1, 2, 3, 4, 5]; const numbersDoubled = [2, 4, , 6, 8, , 10]; const divisible = function (element, index, array) { console.log(array); return element % this?.divisor === 0; }; const areAllDoubledEven = numbersDoubled.every(divisible, { divisor: 2 }); console.log(areAllDoubledEven); console.log("Caller length: " + numbersDoubled.length); /* [ 2, 4, <1 empty item>, 6, 8, <1 empty item>, 10 ] [ 2, 4, <1 empty item>, 6, 8, <1 empty item>, 10 ] [ 2, 4, <1 empty item>, 6, 8, <1 empty item>, 10 ] [ 2, 4, <1 empty item>, 6, 8, <1 empty item>, 10 ] [ 2, 4, <1 empty item>, 6, 8, <1 empty item>, 10 ] true [ 2, 4, <1 empty item>, 6, 8, <1 empty item>, 10 ] */如我们所见,数组有两个空槽,调用方数组的长度为 7。然而numbersDoubled只被打印了 5 次,说明divisible()只被调用了 5 次——因为every()不会为空槽调用回调函数。
提示:这也是
every()与map()/filter()等方法的显著差异之一。对稀疏数组执行“全量校验”时,空槽会被直接跳过,因此every()的结果只反映已填充元素是否满足条件。
与空数组配合使用
如果对一个空数组调用带divisible的every(),它返回true:
const emptyArray = []; const divisible = (element) => { return element % this?.divisor === 0; }; const testEmptyArray = emptyArray.every(divisible, { divisor: 2 }); console.log(testEmptyArray); // true原因在于:空数组中“所有元素”都**空真地(vacuously)**满足“它们是偶数”或任何其他条件。这是数学上“全称量词作用于空集为真”在编程语言中的体现——理解这一点有助于避免写出错误的逻辑判断。
实际场景应用
数据校验
用every()检查所有表单字段是否都已填写:
const fields = ["email@example.com", "password123"]; const allFieldsFilled = fields.every((field) => field.trim() !== ""); console.log(allFieldsFilled); // true if no empty fields类型检查
确认数组中的元素全部为数字:
const data = [1, 2, 3]; const allNumbers = data.every((item) => typeof item === "number"); console.log(allNumbers); // true权限判断
验证团队成员是否都具有 admin 角色:
const team = [{ role: "admin" }, { role: "admin" }]; const allAdmins = team.every((member) => member.role === "admin"); console.log(allAdmins); // true仓库源码中的every()实践
every()在本仓库(Refine)中被大量用于“全量校验”类的逻辑,下面是三个典型的源码级示例,可作为上文各特性的真实印证:
1. 类型检查:推断器(Inferencer)判断数组是否为基本类型数组
在 packages/inferencer/src/field-inferencers/array.ts 中,arrayInfer用value.every(...)判断数组元素是否全部为string或number,从而决定是否为该字段生成基本类型数组的表单/表格组件:
const isBasicArray = Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number");这与上文“类型检查”场景完全对应:只有全部元素都是基本类型,才走基本数组的推断分支。
2. 多条件校验:路由段是否全部匹配
在 packages/core/src/definitions/helpers/router/check-by-segments.ts 中,checkBySegments使用带(segment, index)双参数的every(),逐个校验资源路由的每一段要么是参数(:id之类),要么与当前路由对应段完全相等,全部通过才认为路由匹配:
return resourceRouteSegments.every((segment, index) => { return isParameter(segment) || segment === routeSegments[index]; });这是“多条件 + 索引协同判断”的典型用法,也展示了every()短路特性对路由匹配性能的实际收益——只要有一段不匹配就立即返回false。
3. 复杂对象校验:日期范围过滤器确认所有值都是 Dayjs 对象
在 packages/antd/src/definitions/filter-mappers/index.ts 中,rangePickerFilterMapper先将every(dayjs.isDayjs)作为守卫条件,确认selectedKeys全部为 Dayjs 实例后,才批量转换为 ISO 8601 字符串:
if (selectedKeys.every(dayjs.isDayjs)) { return selectedKeys.map((date: any) => dayjs(date).toISOString()); }这里是“先every()全量校验、后map()批量转换”的组合模式:校验不通过则保持原值返回,避免对非法数据做转换。同时也印证了callbackFn可以直接复用已有函数(dayjs.isDayjs本身就是接收一个参数并返回布尔值的函数)。
关于thisArg与严格模式
every(callback, thisArg)依赖普通函数动态this的特性来注入数据。在 JavaScript 严格模式下,若回调不是通过thisArg指定this,则回调内的this为undefined。本仓库的 TypeScript 配置(各 package 的 tsconfig.json 均开启了"strict": true)正是处于严格语义下运行,因此上文“箭头函数 +thisArg失效”的坑在 Refine 这类严格模式工程中会原样复现——这也是理解该方法行为时需要特别留意的工程现实。
最佳实践小结:
- 需要
thisArg时,回调必须使用普通函数(具名声明或函数表达式),避免箭头函数; - 能用闭包/外部变量替代时,优先考虑闭包方案,可同时规避
this绑定问题并获得更好的可读性; - 明确
every()对空数组恒返回true、对稀疏数组跳过空槽,避免在这些边界场景写出反直觉的判断; - 不要依赖“回调中修改调用方数组”的时机性行为,保持回调纯净(pure)是更稳妥的编码习惯。
常见问题(FAQ)
1. JavaScript 中every方法的用途是什么?
它用于检查数组中的所有元素是否都通过了回调函数中提供的测试。如果全部通过返回true,否则返回false。
2.every方法对空数组有效吗?
有效。它对于空数组总是返回true,因为不存在任何可能不满足条件的元素。
3.every方法会修改数组吗?
不会。方法本身不改变数组,但数组可以在回调函数内部被修改(尽管不推荐依赖这种时机性行为)。
4.every和some有什么区别?
every检查是否所有元素都满足条件,some检查是否至少有一个元素满足条件。前者遇假即停并返回false,后者遇真即停并返回true。
5.every方法能处理对象吗?
可以。你需要在回调函数中访问对象属性来对它们进行测试,例如team.every((member) => member.role === "admin")。
总结
本文深入探讨了 JavaScript 的every()方法,它帮助我们测试数组中的所有项是否都通过了回调函数中实现的测试。我们通过示例看到,回调函数可以接收三个参数,额外的参数可以通过传给every()的第二个参数thisArg绑定到回调的执行上下文中。
我们还发现:如果用箭头语法声明回调函数,其词法环境会破坏thisArg参数到this对象的绑定。因此,凡是在回调中使用this的场景,都应该用非箭头函数来声明回调。结合短路求值、稀疏数组与空数组语义,以及本仓库 packages 中的真实实现,你可以在数据校验、权限判断、类型检查与路由匹配等场景中写出正确且高效的every()代码。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考