头图

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

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() 产生了预期的结果。现在考虑这些例子:

[].every(() => true);           // true
[].every(() => false);          // true

这可能更令人惊讶:无论是返回 true 还是 false 的回调,结果都是一样的。唯一的原因是如果回调没有被调用,every() 的默认值是 true。但是,为什么一个空数组会对 every() 返回 true,当没有值去执行回调函数时呢?

要理解原因,重要的是要看看规范是如何描述这个方法的。

实现 every()

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

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 表现得像数学中的“全称量词”。特别是对于一个空数组,它返回 true。(空集合中的所有元素默认满足任何给定条件是一种空洞真理。)

空洞真理 是指如果给定条件(称为前提)不能满足(即,给定条件不是真的),那么某事是真的。把它转回 JavaScript 方面,every() 对一个空集返回 true 是因为没有办法调用回调。回调代表要测试的条件,如果因为数组中没有值而无法执行它,则 every() 必须返回 true

“全称量词”是数学中一个更大主题的一部分,称为普遍量化,它允许你对数据集合进行推理。鉴于 JavaScript 数组在执行数学计算中的重要性,尤其是与类型化数组一起使用,自然会支持这种操作。而且 every() 不是唯一的例子。

数学和 JavaScript 中的“存在量词”

JavaScript 的 some() 方法实现了存在量化(“存在”有时也称为“存在”或“对于某些”)中的“存在”量词。"存在" 量词声明,对于任何空集合,结果是假。因此,some() 方法对一个空集返回 false,并且它也不执行回调。这里有一些例子(双关语):

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() 方法实现了“存在”。

因此,JavaScript 凭借 every() 和 some() 与众不同。

“全称” every() 的含义

不管你是否认为 every() 的行为违反直觉,这都是值得讨论的。然而,不管你的观点如何,你都需要意识到 every() 的“全称”本质,以避免错误。简而言之,如果你使用 every() 或可能为空的数组时,你应该事先进行明确的检查。例如,如果你有一个依赖数字数组的操作,而空数组会导致操作失败,那么你应该在使用 every() 之前检查数组是否为空:

function doSomethingWithNumbers(numbers) {

    // 首先检查长度
    if (numbers.length === 0) {
        throw new TypeError("Numbers array is empty; this method requires at least one number.");
    }

    // 现在用 every() 检查
    if (numbers.every(isNumber)) {
        operationRequiringNonEmptyArray(numbers);
    }

}

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

结论

虽然我对 every() 对一个空数组的行为感到惊讶,但一旦你理解了这个操作的更广泛上下文以及这个功能在不同语言中的普及,这就讲得通了。如果你对这个行为也感到困惑,那么我建议你在遇到 every() 调用时改变你的阅读方式。不要把 every() 看作是“这个数组的每一项是否满足这个条件?”而是看作是,“数组中是否有任何一项不满足这个条件?”这种思维的转变可以帮助你避免未来在你的 JavaScript 代码中出现错误。

原文作者:Nicholas C. Zakas


Apifox
23 声望4 粉丝

Apifox 是 API 文档、API 调试、API Mock、API 自动化测试一体化平台。Apifox = Postman + Swagger + Mock + JMeter