小心這個陷阱: 為什么JS中的 Every()對空數組總返回 True
JavaScript 語言的核心部分足夠大,以至于我們很容易誤解其某些部分的工作方式。最近在重構一些使用 every() 方法的代碼時,發(fā)現實際上并不理解其背后的邏輯。在我的理解中,我認為回調函數必須被調用并返回true , 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()
產生了預期的結果?,F在考慮以下這些例子:
[].every(() => true); // true
[].every(() => false); // true
這可能更令人驚訝:返回 true 或 false 的回調函數具有相同的結果。這只能發(fā)生的唯一原因是如果回調函數沒有被調用,而 every() 的默認值是 true 。但是,為什么在沒有值來運行回調函數時,空數組會返回 true 給 every() 呢?
要理解為什么,我們需要仔細看看規(guī)范是如何描述這個方法的。
實現 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 。如果數組中沒有任何項目,那么就沒有機會執(zhí)行回調函數,因此,該方法無法返回 false 。
現在的問題是:為什么 every() 會表現出這樣的行為?
在數學和JavaScript中的“對所有”的量詞
MDN頁面 提供了為什么 every() 會對空數組返回 true 的答案:
every 的行為就像數學中的“全稱量詞”。特別是對于空數組,它返回真值。(空集中的所有元素都滿足任何給定條件,這是顯然的真理。)
空真(Vacuous truth)是一個數學概念,意味著如果給定的條件(稱為前件)不能被滿足(即給定的條件不為真),那么某件事就是真的。用JavaScript的術語來說,every() 對于一個空集合返回 true,因為沒有辦法調用回調函數?;卣{函數代表要測試的條件,如果由于數組中沒有值而無法執(zhí)行它,那么 every() 必須返回 true。
“全稱量詞”("for all" quantifier)是數學中更大主題“全稱量化”(universal quantification)的一部分,它允許你對數據集進行推理??紤]到JavaScript數組在進行數學計算方面的重要性,尤其是在使用類型數組(typed arrays)的情況下,內置支持這樣的操作是合理的。而**every()**方法并不是唯一的例子。
在數學和JavaScript中的“存在量詞”
JavaScript的 some() 方法實現了存在量化(existential quantification)中的“存在量詞”(“存在”有時也被稱為“存在”或“對某些”)。這個“存在量詞”規(guī)定,對于任何空集合,結果都是假的。因此,some() 方法對空集合返回 false,并且也不會執(zhí)行回調函數。以下是一些相關的示例:
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() 之前,你應該檢查數組是否為空。
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代碼中出現錯誤。