原文链接:JavaScript WTF: Why does every() return true for empty arrays?
翻译:gpt-4-1106-preview
校对:MonchLee
导读:本文是 Eslint 的作者 Nicholas C. Zakas 的一篇博文,文章从一个 Case 开始:数组的 every 方法对于空数组会返回 true,首先介绍了 every 在语言规范层面的实现,接着介绍了数学中的全称量词、存在量词和空洞定理,阐释了为什么会 JavaScript 会以这种方式来实现 every,由浅入深,最后提出了对 every 独特的理解,非常具有启发性 ❤️
推荐指数:🌟🌟🌟🌟
JavaScript 语言的核心部分非常庞大,以至于很容易误解其某些部分的工作原理。最近我在重构一些使用 every()
方法的代码时,发现我实际上并没有完全理解其背后的逻辑。在我的脑海中,我假设回调函数必须被调用并且对于 every()
来说必须返回 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 中的 “for all” 量词
MDN 页面提供了为什么 every()
对于空数组返回 true
的答案:
every()
行为类似于数学中的 “for all” 量词。特别是对于空数组,它返回true
。(空集的所有元素满足任何给定条件是一种空洞真理。)
空洞真理是一个数学概念,意味着如果给定条件(称为前提)不能被满足(即,给定条件不为真),那么某事是真的。将这个概念回归到 JavaScript 术语中,every()
对于空集返回 true
是因为没有办法调用回调函数。回调函数代表要测试的条件,如果因为数组中没有值而不能执行它,那么 every()
必须返回 true
。
“for all” 量词是数学中称为普遍量化的更大主题的一部分,它允许你对数据集进行推理。考虑到 JavaScript 数组在执行数学计算中的重要性,特别是与类型化数组一起使用时,内置支持此类操作是有意义的。而 every()
并不是唯一的例子。
数学和 JavaScript 中的 “there exists” 量词
JavaScript 的 some()
方法实现了来自存在量化(“there exists” 有时也被称为 “exists” 或 “for some”)的 “exists” 量词。 “exists” 量词声明对于任何空集,结果都是 false
。因此,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()
函数实现了 “for all”,而any()
函数实现了 “there exists”。 - Rust:
Iterator::all()
方法实现了 “for all”,而any()
函数实现了 “there exists”。
因此,JavaScript 与 every()
和 some()
一起处于不错的伴随状态。
“for all” every() 的含义
你是否认为 every()
函数的行为违反直觉,这是一个值得讨论的问题。然而,不管你的看法如何,你都需要意识到 every()
的 “for all” 特性,以避免错误。简而言之,如果你正在使用 every()
或可能为空的数组,你应该事先进行明确的检查。例如,如果你有一个依赖数字数组的操作,并且在数组为空时会失败,那么你应该在使用 every()
之前检查数组是否为空:
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 代码中出错。
写在最后
本文首发于我的 博客,才疏学浅,难免有错误,文章有误之处还望不吝指正!
如果有疑问或者发现错误,可以在评论区进行提问和勘误,
如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。