聊聊 Symbol.iterator 和 for...of

588 阅读5分钟

概念

1. for...of

for...of 是在 ECMA 2015(ECMA-262) 提出的草案,并在在 第六版 中正式成为标准。在已有 for for...in forEach 的情况下为什么还要引入一个新的迭代器?先来看看之前的迭代方案都有些什么特点。

  1. for...in
    • 我们都知道,for...in 遍历返回的是键名,数组的键名应该是数字,但是 for...in 在遍历数组的时候却是返回的字符串;
    var a = [1, 2, 3];
    for (var k in a) {
        console.log(k); // "0", "1", "2"
        console.log(typeof k); // string
    }
    
    • for...in 不仅遍历数字键名,还会遍历手动添加的其它键,甚至包括原型链上的键;
    var a = [1,2,3];
    a["val"] = "key";
    for (var k in a) {
        console.log(k); // "0", "1", "2", "val"
        // 神奇吧,还有更神奇的
    }
    a.__proto__["proto_val"] = "proto_key"; // 给原型链赋值
    for (var k in a) {
        console.log(k); // "0", "1", "2", "val", "proto_val"
    }
    
    • 某些情况下,for...in 循环会以任意顺序遍历键名;
    var o = {
        length: 3,
        2: 2,
        "0": 0,
        1: 1,
    };
    for (var k in o) {
        console.log(k); // "0", "1", "2", "length"
    }
    // 按照预想输出顺序应该是我声明时的顺序,
    // 但是如果在声明对象时如果键名存在数字或者字符串类型的数字(typeof 1 || typeof "1"),
    // 那么在该对象声明后便会自动先排个序
    console.log(JSON.stringify(o)); // "{"0":0,"1":1,"2":2,"length":3}"
    
  2. forEach
    • 这种循环方式 break 命令无效,无法跳出循环,只能利用 return 跳出当次循环;
    var a = [1,2,3];
    a.forEach(function(item) {
        if (item === 1) {
            return; // 只会跳出当次循环,类似continue
        }
        console.log(item);
    })
    
  3. for
    • 这种方式都知道的,最基础的循环,for (var i = 0; i < 10; i++) { } 一眼过去就知道用这种方式循环还是比较麻烦的;
  4. for...of
    • for...in 一样的简洁语法,但是没有对应的缺点;
    • forEach 不同,break continuereturn 可以配合使用;
    • 提供遍历所有数据结构统一的接口;

2. Symbol.iterator

在可被 for...of 迭代遍历的数据集合的原型链中都存在该属性为 key 的方法。

Symbol.iterator in Array.prototype; // true
Symbol.iterator in Set.prototype; // true
Symbol.iterator in Map.prototype; // true
// Generator 对象
function* generator() {
    yield 1;
    yield 2;
}
Symbol.iterator in generator(); // true
for (var i of generator()) {
    console.log(i); // 1, 2
}

// 对象结构无法使用 for...of 遍历
Symbol.iterator in Object.prototype; // false

还有一些特殊的数据类型,类似数组对象 TypedArray,比如:函数参数集合 arguments

类似数组对象的定义:有 length 属性和若干索引属性对象。

并不是所有类似数组对象都存在 Iterator 接口,可以使用 Array.from() 将其转为数组。

/**
 * 类似数组对象
*/
Symbol.iterator in String.prototype; // true
Symbol.iterator in NodeList.prototype; // true
/**
 * arguments
*/
// ES 5
(function() {
    return Symbol.iterator in arguments
})("a", "b", "c") // true
// ES 6
((...args) => Symbol.iterator in args )("a", "b", "c") // true

3. Symbol.iterator 有什么用

遍历器(iterator)属性是一种接口,为各种数据结构提供一个统一的访问机制。不管何种数据结构,只要部署了 Iterator 接口,就能完成遍历操作 。

作用:

  1. 为各种数据结构提供一个便捷的、统一的访问接口;
  2. 使得数据结构的成员 item 按照次序排列;
  3. 主要提供给 for...of 消费

遍历过程:

  1. 创建一个指针对象,指向当前数据结构起始位置。遍历器对象本质上就是一个指针对象。
  2. 第一次调用指针对象的 next 方法,指针就指向第一个成员。
  3. 第二次调用 next 方法,指针就指向第二个成员。
  4. ......循环执行第三步,直到指针指向该数据结构结束位置。

next 方法返回的对象结构:

next() {
    return {
        value: item,    // 当前下标指向的位置成员
        done: Boolean,  // 遍历是否结束,true 为结束
    }
}

用代码说话,举个栗子:

let a = ['1', '2', '3'];
let iter = a[Symbol.iterator](); // 执行数组的 iterator 属性方法

iter.next(); // { value: '1', done: false }
iter.next(); // { value: '2', done: false }
iter.next(); // { value: '3', done: false }
iter.next(); // { value: undefined, done: true }

对类似数组对象可以直接引用数组的 Iterator 接口。

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]

var iter = document.querySelectorAll('div')[Symbol.iterator]() // 可以执行了
iter.next(); // 第一个 div

// 另外一种类似数组对象调用 Symbol.iterator 的例子
var iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (var item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}

4. 在什么情况下会调用 Symbol.iterator 接口

  1. 对数组或者 Set 数据类型解构赋值时会默认触发;
    var iterable = ['a', 'b', 'c'];
    iterable[Symbol.iterator] = function() {
        var self = this;
        var v = 0;
        console.log("触发了 Iterator");
        //返回iterator
        return {
            next: function () {
                return v < self.length ?
                       { value: self[v++] } :
                       { done: true };
            }
        };
    };
    var [a, b, c] = iterable; // 触发了 Iterator
    // a => 'a', b => 'b', c => 'c'
    
  2. 扩展运算符也会默认调用 Iterator 接口,可以利用该特性将部署了 Iterator 接口的对象使用扩展运算符转为数组;
    var iterable = {
        [Symbol.iterator]: () => {
            var v = 0;
            console.log("触发了 Iterator");
            //返回iterator
            return {
                next: function () {
                    return {
                        value: ++v,
                        done: v > 3
                    };
                }
            };
        }
    }
    let arr = [...iterable]; // "触发了 Iterator"
    // arr => [1, 2, 3]
    
  3. yield* 后面跟一个可遍历结构数据也会默认触发;
  4. 所有接受数组为参数的方法都调用了遍历器接口
    • Array.from
    • for...of
    • Promise.all
    • Promise.race
    • Map Set WeakMap WeakSet

5. 总结

Symbol.iterator 就是为了给 for...of 循环提供一个统一的遍历接口,for...of 能遍历所有提供了 Symbol.iterator 接口的数据集合;for...of 也是为了让遍历更优雅,提升代码的可读性,毕竟 for...of 的写法比起 for 表达式更简洁。