小心这个陷阱: 为什么JS中的 every()对空数组总返回 true

首发于公众号 大迁世界,欢迎关注。📝 每周7篇实用的前端文章 🛠️ 分享值得关注的开发工具 😜分享个人创业过程中的趣事

JavaScript 语言的核心部分足够大,以至于我们很容易误解其某些部分的工作方式。最近在重构一些使用 every() 方法的代码时,发现实际上并不理解其背后的逻辑。在我的理解中,我认为回调函数必须被调用并返回trueevery() 才会返回 true ,但实际上并非如此。对于一个空数组, every() 无论回调函数是什么都会返回 true ,因为那个回调函数从未被调用过。考虑以下情况:

scss 复制代码
function isNumber(value) {
    return typeof value === "number";
}

[1].every(isNumber);            // true
["1"].every(isNumber);          // false
[1, 2, 3].every(isNumber);      // true
[1, "2", 3].every(isNumber);    // false
[].every(isNumber);             // true

在这个例子的每个情况中,对 every() 的调用都会检查数组中的每个项目是否为数字。前四次调用相当直接, every() 产生了预期的结果。现在考虑以下这些例子:

javascript 复制代码
[].every(() => true);           // true
[].every(() => false);          // true

这可能更令人惊讶:返回 truefalse 的回调函数具有相同的结果。这只能发生的唯一原因是如果回调函数没有被调用,而 every() 的默认值是 true 。但是,为什么在没有值来运行回调函数时,空数组会返回 trueevery() 呢?

要理解为什么,我们需要仔细看看规范是如何描述这个方法的。

实现 every()

ECMA-262 定义了一个 Array.prototype.every() 算法,大致可以翻译成这段JavaScript代码:

ini 复制代码
Array.prototype.every = function(callbackfn, thisArg) {

    const O = this;
    const len = O.length;

    if (typeof callbackfn !== "function") {
        throw new TypeError("Callback isn't callable");
    }

    let k = 0;

    while (k < len) {
        const Pk = String(k);
        const kPresent = O.hasOwnProperty(Pk);

        if (kPresent) {
            const kValue = O[Pk];
            const testResult = Boolean(callbackfn.call(thisArg, kValue, k, O));

            if (testResult === false) {
                return false;
            }
        }

        k = k + 1;
    }

    return true;
};

从代码中,你可以看到 every() 假设结果是 true ,并且只有在回调函数对数组中的任何一项返回 false 时才返回 false 。如果数组中没有任何项目,那么就没有机会执行回调函数,因此,该方法无法返回 false

现在的问题是:为什么 every() 会表现出这样的行为?

在数学和JavaScript中的"对所有"的量词

MDN页面 提供了为什么 every() 会对空数组返回 true 的答案:

every 的行为就像数学中的"全称量词"。特别是对于空数组,它返回真值。(空集中的所有元素都满足任何给定条件,这是显然的真理。)

空真 (Vacuous truth)是一个数学概念,意味着如果给定的条件(称为前件)不能被满足(即给定的条件不为真),那么某件事就是真的。用JavaScript的术语来说,every() 对于一个空集合返回 true,因为没有办法调用回调函数。回调函数代表要测试的条件,如果由于数组中没有值而无法执行它,那么 every() 必须返回 true

"全称量词"("for all" quantifier)是数学中更大主题"全称量化"(universal quantification)的一部分,它允许你对数据集进行推理。考虑到JavaScript数组在进行数学计算方面的重要性,尤其是在使用类型数组(typed arrays)的情况下,内置支持这样的操作是合理的。而**every()**方法并不是唯一的例子。

在数学和JavaScript中的"存在量词"

JavaScript的 some() 方法实现了存在量化(existential quantification)中的"存在量词"("存在"有时也被称为"存在"或"对某些")。这个"存在量词"规定,对于任何空集合,结果都是假的。因此,some() 方法对空集合返回 false,并且也不会执行回调函数。以下是一些相关的示例:

scss 复制代码
function isNumber(value) {
    return typeof value === "number";
}

[1].some(isNumber);            // true
["1"].some(isNumber);          // false
[1, 2, 3].some(isNumber);      // true
[1, "2", 3].some(isNumber);    // true
[].some(isNumber);             // false
[].some(() => true);           // false
[].some(() => false);          // false

其他语言中的量化

JavaScript并不是唯一实现了集合或可迭代对象的量化方法的编程语言:

  • Python: all() 函数实现了"对所有" ,而 any() 函数实现了"存在" 。
  • Rust: Iterator::all() 方法实现了"对所有" ,而 any() 函数实现了"存在" 。

"全称量词"(for all)的 every() 方法的含义与影响

无论你是否认为 every() 方法的行为违反直觉都是可以讨论的。然而,无论你的观点如何,你都需要了解 every() 的"全称量词"(for all)特性以避免错误。简而言之,如果你使用 every() 方法或可能为空的数组,你应该事先进行明确的检查。例如,如果你有一个依赖于数字数组的操作,并且在数组为空时会失败,那么在使用 every() 之前,你应该检查数组是否为空。

scss 复制代码
function doSomethingWithNumbers(numbers) {

    // first check the length
    if (numbers.length === 0) {
        throw new TypeError("Numbers array is empty; this method requires at least one number.");
    }

    // now check with every()
    if (numbers.every(isNumber)) {
        operationRequiringNonEmptyArray(numbers);
    }

}

再次强调,只有当你有一个数组在为空时不应该被用于操作时,这才重要;否则,你可以避免这个额外的检查。

结论

当我第一次看到 every() 在空数组上的行为时,我感到很惊讶,但一旦你理解了这个操作的更大背景和这个功能在各种语言中的广泛应用,就会觉得它是有道理的。如果你也对这个行为感到困惑,那么我建议你改变阅读 every() 调用的方式。不要把 every() 理解为"这个数组中的每一项是否都符合这个条件?"而应该理解为"这个数组中是否有任何一项不符合这个条件?"这种思维方式的转变可以帮助你避免在未来的JavaScript代码中出现错误。

交流

首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答

本文 GitHub github.com/qq449245884... 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

相关推荐
伍哥的传说8 分钟前
Lodash-es 完整开发指南:ES模块化JavaScript工具库实战教程
大数据·javascript·elasticsearch·lodash-es·javascript工具库·es模块·按需导入
@菜菜_达9 分钟前
Lodash方法总结
开发语言·前端·javascript
GISer_Jing14 分钟前
低代码拖拽实现与bpmn-js详解
开发语言·javascript·低代码
YAY_tyy19 分钟前
基于 Vue3 + VueOffice 的多格式文档预览组件实现(支持 PDF/Word/Excel/PPT)
前端·javascript·vue.js·pdf·word·excel
Yvonne爱编码39 分钟前
AJAX入门-AJAX 概念和 axios 使用
前端·javascript·ajax·html·js
Pu_Nine_92 小时前
10 分钟上手 ECharts:从“能跑”到“生产级”的完整踩坑之旅
前端·javascript·echarts·css3·html5
li35744 小时前
React 核心 Hook 与冷门技巧:useReducer、useEffect、useRef 及 is 属性全解析
前端·javascript·react.js
快乐是Happy4 小时前
分享一个非常实用的防止重复提交操作
前端·javascript
可子是我的小猫4 小时前
【JS】模块(二)
javascript
云枫晖4 小时前
JS核心知识-执行上下文
前端·javascript