Array.prototype.forEach(callback) 的 callback 到底執行了幾次?

小魚二發表於2018-03-21

原文連結

事情的起源是這樣的, 同事發給我兩段程式碼, 如下:

var a = [1, 2, 3, 1, 2, 3];
a.forEach((item, index) => {
    console.log(index, item);
    if (item === 1) {
    	a.splice(index, 1);
    }
});
// 輸出
// 0 1
// 1 3
// 2 1
// 3 3



var a = [1, 2, 3, 1, 2, 3];
a.forEach((item, index) => {
    console.log(index, item);
    if (item === 1) {
    	a.push(1);
    }
});
// 輸出
// 0 1
// 1 2
// 2 3
// 3 1
// 4 2
// 5 3
複製程式碼

為什麼第一個輸出四次, 第二個不輸出8次呢?

其實這樣的事情在我們平常寫程式碼的時候也經常發生, 如果這個改成 for 迴圈, 或許完全不一樣. 那麼 forEachcallback 到底執行了多少次呢?

這樣的事情當然要看規範了, Array.prototype.forEach() 中文

forEach 方法按升序為陣列中含有效值的每一項執行一次callback 函式,那些已刪除(使用delete方法等情況)或者未初始化的項將被跳過(但不包括那些值為 undefined 的項)(例如在稀疏陣列上)。

forEach 遍歷的範圍在第一次呼叫 callback 前就會確定。呼叫forEach 後新增到陣列中的項不會被 callback 訪問到。如果已經存在的值被改變,則傳遞給 callback 的值是 forEach 遍歷到他們那一刻的值。已刪除的項不會被遍歷到。如果已訪問的元素在迭代時被刪除了(例如使用 shift()) ,之後的元素將被跳過

這裡面感覺最重要的是:

  • forEach 遍歷的範圍在第一次呼叫 callback 前就會確定
  • 如果已經存在的值被改變,則傳遞給 callback 的值是 forEach 遍歷到他們那一刻的值

看不懂? show me the code

// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
// 如果 Array.prototype.forEach 沒有定義的話
if (!Array.prototype.forEach) {

    Array.prototype.forEach = function (callback/*, thisArg*/) {

        // T 為 callback 的指向, 如果指定的話, 看 step-5
        // k 為 迴圈的索引
        var T, k;

        if (this == null) {
            throw new TypeError('this is null or not defined');
        }

        // 1. Let O be the result of calling toObject() passing the
        // |this| value as the argument.
        // @see https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object
        // Object建構函式為給定值建立一個物件包裝器。如果給定值是 null 或 undefined,將會建立並返回一個空物件,否則,將返回一個與給定值對應型別的物件。
        // 當以非建構函式形式被呼叫時,Object 等同於 new Object()。
        var O = Object(this);

        // 2. Let lenValue be the result of calling the Get() internal
        // method of O with the argument "length".
        // 3. Let len be toUint32(lenValue).
        // @see https://stackoverflow.com/questions/8286925/whats-array-length-0-used-for
        // 保證 len 為一個小於 2^32 的整數
        // 這裡需要注意, step-7 的終止條件是 k < len;
        // 所以, forEach 遍歷的範圍在第一次呼叫 callback 前就會確定
        var len = O.length >>> 0;

        // 4. If isCallable(callback) is false, throw a TypeError exception.
        // See: http://es5.github.com/#x9.11
        // 保證 callback 是個函式
        if (typeof callback !== 'function') {
            throw new TypeError(callback + ' is not a function');
        }

        // 5. If thisArg was supplied, let T be thisArg; else let
        // T be undefined.
        if (arguments.length > 1) {
            T = arguments[1];
        }

        // 6. Let k be 0.
        k = 0;

        // 7. Repeat while k < len.
        while (k < len) {

            // 第 k 項
            var kValue;

            // a. Let Pk be ToString(k).
            //    This is implicit for LHS operands of the in operator.
            // b. Let kPresent be the result of calling the HasProperty
            //    internal method of O with argument Pk.
            //    This step can be combined with c.
            // c. If kPresent is true, then
            // 保證 k 這個索引是 O 的屬性
            if (k in O) {

                // i. Let kValue be the result of calling the Get internal
                // method of O with argument Pk.
                // 賦值
                kValue = O[k];

                // ii. Call the Call internal method of callback with T as
                // the this value and argument list containing kValue, k, and O.
                // 呼叫 callback, T 為 callback 繫結的 this, 引數分別是 item, index, 和 array 本身
                callback.call(T, kValue, k, O);
            }
            // d. Increase k by 1.
            k++;
        }
        // 8. return undefined.
    };
}
複製程式碼

剛剛說的兩條分別對應 step-3

 // 3. Let len be toUint32(lenValue).
 var len = O.length >>> 0;

複製程式碼

和 step-7-c

if (k in O) 
複製程式碼

回到第一題

var a = [1, 2, 3, 1, 2, 3];
a.forEach((item, index) => {
    console.log(index, item);
    if (item === 1) {
    	a.splice(index, 1);
    }
});
複製程式碼
1. a = [1,2,3,1,2,3]; len = 6
2. k = 0; console.log(0, 1);
3. splice(0, 1) ---> a = [2,3,1,2,3]
4. k = 1; console.log(1, 3);
5. k = 2; console.log(2, 1);
6. splice(2, 1) ---> a = [2,3,2,3];
7. k = 3; console.log(3, 3);
8. k = 4; k not in a;
9. k = 5; k not in a;
複製程式碼

第二題比較簡單, 新新增的兩個 1 都不會遍歷

所以兩種情況的 while 迴圈都是 6 次

但是第一種由於 '4' '5' 都不在 array 裡面, 所以 callback 只執行了 4 次

第二種情況 callback 執行了 6 次

好啦, 你聽明白了嘛~

參考資料

相關文章