从forEach到迭代器

3,275 阅读8分钟

本文从使用 forEach 对数组进行遍历开始说起,粗略对比使用 forEach , for...in , for...of 进行遍历的差异,并由此引入 ES6 中 *可迭代对象/迭代器 *的概念,并对其进行粗略介绍。

forEach

forEach 方法按升序为数组中的有效值的每一项执行一次callback 函数

特点

  1. 只会对“有效值”进行操作,也就是说遍历的时候会跳过已删除或者未初始化的项,即会跳过那些值为 empty 的内容。(undefined 不会被跳过哦)。

image.png | left | 426x231

  1. forEach 的第二个参数 thisArg 传入后可以改变 callback 函数的 this 值,如果不传则为 undefined 。当然, callback 所拿到的 this 值也受普遍规律的支配:这意味着如果 callback 是个箭头函数,则 thisArg 会被忽略。(因为箭头函数已经在词法上绑定了this值,不能再改了)
  2. 不建议在循环中增/删数组内容:首先,forEach 遍历的范围在第一次调用 callback 前就会确定,这意味着调用forEach 后添加到数组中的项不会被 callback 访问到。同时,由于 forEach 的遍历是基于下标的(可以这么理解,并能从 Polyfill 中看到这一实现),那么在循环中删了数组几项内容则会有奇怪的事情发生:比如下图中,在下标为 1 的时候执行了 shift() ,那么原来的第 3 项变为了第 2 项,原来的第 2 项则被跳过了。

image.png | left | 404x206

缺点

  1. 无法中断或跳出:如果想要中断/跳出,可以考虑使用如下两种方式:
    1. 使用 for / for..of
    2. 使用 Array.prototype.every() / Array.prototype.some() 并返回 false / true 来进行中断/跳出
  2. 无法链式调用(因为返回了 undefined

for...in

for...in 循环实际是为循环”enumerable“对象而设计的:

for...in 语句以任意顺序遍历一个对象的 可枚举属性 。对于每个不同的属性,语句都会被执行。

特点

  1. 遍历可枚举属性:for...in 循环将遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性(更接近原型链中对象的属性覆盖原型属性)。
  2. 可以被中断

缺点

  1. 会访问到能访问到的所有的 __ 可枚举属性__ ,也就是说会包括那些原型链上的属性。如果想要仅迭代自身的属性,那么在使用 for...in 的同时还需要配合 getOwnPropertyNames()hasOwnProperty()
  2. 不能保证for ... in将以任何特定的顺序返回索引,比如在 IE 下可能会乱来。
  3. 不建议用于迭代 Array
    1. 不一定保证按照下标顺序
    2. 遍历得到的下标是字符串而不是数字

for...of

for...of 的本质是在 可迭代对象(iterable objects) 上调用其 迭代方法 创建一个迭代循环,并执行对应语句。可以迭代 数组/字符串/类型化数组(TypedArray)/Map/Set/generators/类数组对象(NodeList/arguments) 等。需要注意的是, Object 并不是一个可迭代对象。

for...of 是 ES6 中新出炉的,其弥补了 forEachfor...in 的诸多缺点:

  1. 可以使用breakthrow 或return 等终止
  2. 能正常迭代数组(依赖数组默认的迭代行为)

区别

那么我们简单的几句话说明一下 for...offor...in 的区别:

  • for...in 语句以原始插入顺序(还不一定保证)迭代对象的 可枚举属性
  • for...of 语句遍历 可迭代对象 定义要迭代的数据。
  • for...of  得到的是 值(value), 而 for...in 得到的是 键(key)。

那么扯到了 可迭代对象 ,就不得不说说 ES6 中新增的与 可迭代对象/迭代器 有关东西了。

可迭代对象/迭代器

*iteration *是 ES6 中新引入的遍历数据的机制,其核心概念是:iterable(可迭代对象)  和 iterator(迭代器):

  • *iterable(可迭代对象):*一种希望被外界访问的内部元素的数据结构,实现了 Symbol.iterator 方法
  • *iterator(迭代器):*用于遍历数据结构元素的指针

可迭代协议(iterable protocol)

首先,可迭代协议允许 JavaScript 对象去定义或定制它们的迭代行为。而如 Array 或 Map 等内置可迭代对象有默认的迭代行为,而如 Object 则没有。(所以为什么不能对 Objectfor...of )再具体一点,即对象或其原型上有 [Symbol.iterator] 的方法,用于返回一个对象的无参函数,被返回对象符合迭代器协议。然后在该对象被迭代的时候,调用其 [Symbol.iterator] 方法获得一个在迭代中使用的迭代器。

首先可以看见的是 Array 和 Map 在原型链上有该方法,而 Object 则没有。这印证了上面对于哪些可以用于 for...of  的说法。

image.png | left | 324x151

如果我们非要想用 for...ofObject 所拥有的属性进行遍历,则可使用内置的 Object.keys() 方法:

for (const key of Object.keys(someObject)) {
  console.log(key + ": " + someObject[key]);
}

或者如果你想要更简单得同时得到键和值,可以考虑使用 Object.entries()

for (const [key, value] of Object.entries(someObject)) {
  console.log(key + ": " + value);
}

其次,有如下情况会使用可迭代对象的迭代行为:

  1. for...of
  2. 扩展运算符(Spread syntax)
  3. yield*
  4. 解构赋值(Destructuring assignment )
  5. 一些可接受可迭代对象的内置API(本质是这些接口在接收一个数组作为参数的时候会调用数组的迭代行为)
    1. Array.from()
    2. Map(), Set(), WeakMap(), WeakSet()
    3. Promise.all() /Promise.race() 等

迭代器协议(iterator protocol)

上文说到返回了一个迭代器用于迭代,那我们就来看看符合什么样规范的才算一个 迭代器 。 只需要实现一个符合如下要求的 next 方法的对象即可:

属性
next
返回一个对象的无参函数,被返回对象拥有两个属性:
  • done (boolean)
    • 如果迭代器已经经过了被迭代序列时为 true。这时 value 可能描述了该迭代器的返回值。
    • 如果迭代器可以产生序列中的下一个值,则为 false。这等效于连同 done 属性也不指定。
  • value - 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。

本质上,在使用一个迭代器的时候,会不断调用其 next() 方法直到返回 done: true

自定义迭代行为

既然符合可迭代协议的均为可迭代对象,那接下来就简单自定义一下迭代行为:

// 让我们的数组倒序输出 value
const myArr = [1, 2, 3];
myArr[Symbol.iterator] = function () {
    const that = this;
    let index = that.length;
    return {
        next: function () {
            if (index > 0) {
                index--;
                return {
                    value: that[index],
                    done: false
                };
            } else {
                return {
                    done: true
                };
            }
        },
    };
};
[...myArr]; // [3, 2, 1]
Array.from(myArr) // [3, 2, 1]

一句说明可迭代对象和迭代器的关系

当一个__可迭代对象__需要被迭代的时候,它的 Symbol.iterator 方法被无参调用,然后返回一个用于在迭代中获得值的迭代器。 换句话说,一个对象(或其原型)上有符合标准的 Symbol.iterator 接口,那他就是 可迭代的(Iterator) ,调用这个接口返回的对象就是一个 迭代器

image.png | left | 657x184

关闭迭代器

上文提到说 for...offorEach 好在其可以被“中断”,那么对于在 for...of 中中断迭代,其本质是中断了迭代器,迭代器在中断后会被关闭。说到这里,就继续说一下迭代器关闭的情况了。

首先,迭代器的关闭分为两种情况:

  1. Exhaustion:当被持续调用 next() 方法直到返回 done: true ,也就是迭代器正常执行完后关闭
  2. Closing:通过调用 return() 方法来告诉迭代器不打算再调用 next() 方法

那么什么时候会调用迭代器的 return 方法呢:

  1. 首先,return() 是个可选方法,只有具有该方法的迭代器才是 可关闭的(closable)
  2. 其次,只有当没有 Exhaustion 时才应该调用 return() ,如 breakthrow 或 return

最后,return() 方法中也不是想怎么写就怎么写的,也有自己的要求, return()方法需要符合以下规范:

  1. return(x) 通常应该返回如 { done: true, value: x } 的结果,如果返回的不是个对象则会报错
  2. 调用return()后, next()返回的对象也应该是 done:true (这就是为什么有一些迭代器在 for...of 循环中中断后无法再次使用的原因,比如 Generator

同时,需要额外注意的是,及时在收到迭代器最后一个值后调用 break 等,也会触发 return()

function createIterable() {
    let done = false;
    const iterable = {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (!done) {
                done = true;
                return {
                    done: false,
                    value: 'a'
                };
            } else {
                return {
                    done: true,
                    value: undefined
                };
            }
        },
        return () {
            console.log('return() was called!');
            return {
                done: true,
                value: undefined
            };
        },
    };
    return iterable;
}
for (const value of createIterable()) {
    console.log(value);
    break;
}

生成器(Generator)

既是迭代器也是可迭代对象

上文 迭代器协议 中提到的返回的拥有 next() 方法的对象和我们在 Generator 中使用的 next() 方法似乎一模一样。确实, Generator 符合可迭代协议和迭代器协议的。

因为 Generator 既有符合规范的 next() (迭代器协议)方法,也有 Symbol.iterator (可迭代协议)方法,因此它 既是迭代器也是可迭代对象

可关闭的(closable

默认情况下,Generator对象是可关闭的。因此在用 for...of 时中断迭代后,无法再次对原有 Generator对象进行迭代。(因为调用return()后, next()返回的对象也应该是 done:true

当然,既然是默认情况,我们就可以想办法让其无法被关闭: 可以通过包装一下迭代器,将迭代器本身/原型上的 return() 方法被重写掉

class PreventReturn {
    constructor(iterator) {
        this.iterator = iterator;
    }
    [Symbol.iterator]() {
        return this;
    }
    next() {
        return this.iterator.next();
    }
    // 重写掉 return 方法
    return (value = undefined) {
        return {
            done: false,
            value
        };
    }
}

更多关于 Generator 的内容就不在本篇进行阐述,有机会将单独作为一篇慢慢讲。

参考