事情的起源是這樣的, 同事發給我兩段程式碼, 如下:
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 迴圈, 或許完全不一樣. 那麼 forEach
的 callback
到底執行了多少次呢?
這樣的事情當然要看規範了, 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 次
好啦, 你聽明白了嘛~
參考資料