小心這個陷阱: 為什麼JS中的 every()對空陣列總返回 true

發表於2023-09-20
首發於公眾號 大遷世界,歡迎關注。? 每週7篇實用的前端文章 ?️ 分享值得關注的開發工具 ?分享個人創業過程中的趣事

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

這可能更令人驚訝:返回 truefalse 的回撥函式具有相同的結果。這隻能發生的唯一原因是如果回撥函式沒有被呼叫,而 every() 的預設值是 true 。但是,為什麼在沒有值來執行回撥函式時,空陣列會返回 trueevery() 呢?

要理解為什麼,我們需要仔細看看規範是如何描述這個方法的。

實現 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 的行為就像數學中的“全稱量詞”。特別是對於空陣列,它返回真值。(空集中的所有元素都滿足任何給定條件,這是顯然的真理。)

空真(Vacuous truth)是一個數學概念,意味著如果給定的條件(稱為前件)不能被滿足(即給定的條件不為真),那麼某件事就是真的。用JavaScript的術語來說,every() 對於一個空集合返回 true,因為沒有辦法呼叫回撥函式。回撥函式代表要測試的條件,如果由於陣列中沒有值而無法執行它,那麼 every() 必須返回 true

“全稱量詞”("for all" quantifier)是數學中更大主題“全稱量化”(universal quantification)的一部分,它允許你對資料集進行推理。考慮到JavaScript陣列在進行數學計算方面的重要性,尤其是在使用型別陣列(typed arrays)的情況下,內建支援這樣的操作是合理的。而every()方法並不是唯一的例子。

在數學和JavaScript中的“存在量詞”

JavaScript的 some() 方法實現了存在量化(existential quantification)中的“存在量詞”(“存在”有時也被稱為“存在”或“對某些”)。這個“存在量詞”規定,對於任何空集合,結果都是假的。因此,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() 函式實現了“存在” 。

“全稱量詞”(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程式碼中出現錯誤。

交流

首發於公眾號 大遷世界,歡迎關注。? 每週一篇實用的前端文章 ?️ 分享值得關注的開發工具 ❓ 有疑問?我來回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

相關文章