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

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

JavaScript 语言的核心部分足够大,以至于我们很容易误解其某些部分的工作方式。最近在重构一些使用 every() 方法的代码时,发现实际上并不理解其背后的逻辑。在我的理解中,我认为回调函数必须被调用并返回true , every() 才会返回 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

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

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

实现 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... 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

相关推荐
谢亮_vipxieliang19 分钟前
深入理解 JavaScript 数据类型与类型转换
开发语言·javascript·ecmascript
程序员Sunday10 小时前
JavaScript 事件循环面试题,宏任务与微任务怎么执行|Sunday面试指南
开发语言·javascript·面试·校招·事件循环·程序员sunday
默_笙11 小时前
🌊 向量库和 ES 都查不出"关系",我只好给奶茶建了一张人脉网
前端·javascript
思无邪6612 小时前
用 AI 做 JS 逆向:从抓包到复现的完整方法论
开发语言·javascript·人工智能
福兮说13 小时前
浏览器里把 iPhone 的 HEIC 转成 JPG:Chrome 解不开、转完大了七成、拍摄时间全丢,六个坑实测
前端·javascript·图像处理·chrome·ios·iphone·heic
可乐鸡翅yeah_14 小时前
video.js 集成 hls.js 开发 M3U8 播放器,新手高频踩坑
开发语言·前端·javascript·后端·ecmascript·m3u8·音视频在线播放
用户1179536547816 小时前
TRAE vs Claude Code 性能对比:从代码生成、多文件重构到底层吞吐,谁更适合你的日常开发?
javascript
kill52217 小时前
useRef
前端·javascript·react.js
米多科技17 小时前
我做了一个 3D 虚拟世界基底,在上面再搭 3D 世界或元宇宙,事半功倍
javascript·人工智能·github
霍营_托尼18 小时前
纯前端播 40GB 本地视频?我把浏览器改造成了「磁盘流式」播放器
前端·javascript·音视频开发