Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
xiaoyu2er opened this issue Mar 21, 2018 · 0 comments
Open

Comments

@xiaoyu2er
Copy link
Owner

xiaoyu2er commented Mar 21, 2018

原文链接

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

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 次

好啦, 你听明白了嘛~

参考资料

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant