JavaScript 語言的核心足夠大,以至於很容易誤解其某些部分的工作方式。我最近在重構一些使用 every() 方法的代碼時發現,我實際上並沒有完全理解其背後的邏輯。在我的腦海中,我假設回調函數必須被調用並返回 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() 產生了預期的結果。現在考慮這些例子:
[].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