尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入理解 JavaScript `Array.prototype.every()`:签名、`thisArg`、短路求值与稀疏数组行为

深入理解 JavaScript `Array.prototype.every()`:签名、`thisArg`、短路求值与稀疏数组行为 深入理解 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/refineArray.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.mdsome()互为对照。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 则返回trueconst 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与someFeatureeveryMethodsomeMethodPurposeTests ifallelements pass a conditionTests ifat least oneelement passes a conditionReturnstrueif all elements pass the testtrueif any element passes the testStops CheckingAt the first failing elementAt the first passing elementEmpty ArraysAlways returnstrueAlways 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()它返回trueconst emptyArray []; const divisible (element) { return element % this?.divisor 0; }; const testEmptyArray emptyArray.every(divisible, { divisor: 2 }); console.log(testEmptyArray); // true原因在于空数组中“所有元素”都**空真地vacuously**满足“它们是偶数”或任何其他条件。这是数学上“全称量词作用于空集为真”在编程语言中的体现——理解这一点有助于避免写出错误的逻辑判断。实际场景应用数据校验用every()检查所有表单字段是否都已填写const fields [emailexample.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是更稳妥的编码习惯。常见问题FAQ1. 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),仅供参考
返回列表