xiaoyu2er / blog

小鱼二的博客, 喜欢的话请点star :D
337 stars 37 forks source link

Array.prototype.forEach(callback) 的 callback 到底执行了几次? #11

Open xiaoyu2er opened 6 years ago

xiaoyu2er commented 6 years ago

原文链接

事情的起源是这样的, 同事发给我两段代码, 如下:

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()) ,之后的元素将被跳过

这里面感觉最重要的是:

看不懂? 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 次

好啦, 你听明白了嘛~

参考资料