愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

《ECMAScript6入门》学习笔记之Iterator和for-of

一、基本概念

iterator(迭代器)提供一种统一的接口,用以遍历一个集合型的数据结构。只要一个数据结构实现了iterator接口,那么它就能够:
1)使用for-of遍历
2)被展开运算符(...)展开
使用Typescript来描述迭代器接口的话,可以表示如下:

interface Iterable {
    [Symbol.iterator](): Iterator
}

interface Iterator {
    next(value?: any): IterationResult
}

interface IterationResult {
    value: any,
    done: boolean
}

这说明:
1)如果一个对象要实现iterator接口,那么它需要拥有[Symbol.iterator]方法
2)[Symbol.iterator]()方法的返回的是一个迭代器对象,迭代器对象中应该有next()方法
3)next()方法的返回值是一个对象,形式如:{ value: any, done: boolean }


二、基本用法

当我们实现了iterator接口后,我们就可以使用for-of来访问数据结构了,如:

const ds = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3
};
ds[Symbol.iterator] = function() {
    let i = 0;
    const that = this;
    return {
        next() {
            return { value: that[i], done: ++i > that.length };
        }
    }
}
for (let x of ds) {
    console.log(x);
}
/*
输出:
a
b
c
*/

如果在对象自身上没有部署[Symbol.iterator]()方法,那么其原型链中存在的这个方法,也是可以的,如:

function F(){}
F.prototype = {
    constructor: F,
    [Symbol.iterator]() {
        let i = 0;
        return {
            next() {
                let iterator = { value: undefined, done: true };
                if (i < 3) {
                    iterator.value = i++;
                    iterator.done = false;
                }
                return iterator;
            }
        };
    }
}
const f = new F();
for (let x of f) {
    console.log(x);
}
/*
输出:
0
1
2
*/

对于Array-Like的结构(即拥有length属性的对象),可以用如下的方法便捷地使用数组的iterator接口,如:

const obj = { 0: 'a', 1: 'b', length: 2 };
[...obj]; // Uncaught TypeError: obj is not iterable
// 此时,我们可以这么做:
obj[Symbol.iterator] = Array.prototype[Symbol.iterator];
[...obj]; // ['a', 'b']

当然,也可以写作:

obj[Symbol.iterator] = [][Symbol.iterator];

不过,前者要快一些。
如果一个[Symbol.iterator]()的返回值不是一个遍历器对象,那么会报错:

const obj = {
    [Symbol.iterator]() {
        return 1;
    }
}
[...obj]; // TypeError: Result of the Symbol.iterator method is not an object

注意:遍历器对象中,可以缺省done: false或者value: undefined,因为这是默认值


三、Iterator接口与Generator函数

iterator接口可以和generator函数一起使用,如:

const obj = {
    * [Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
        yield* [4, 5, 6];
    }
}
[...obj]; // 得到 [1, 2, 3, 4, 5, 6]


四、遍历器对象的return()throw()

return()方法的作用是:当使用for-of时,如果循环提前退出(使用break或者抛出错误),那么就会被调用,如:

const obj = {
    [Symbol.iterator]() {
        return {
            next() {
                console.log('next() was called');
                return { done: false };
            },
            return() {
                console.log('return() was called');
                return { done: true };
            }
        }
    }
}

所以有:

for (let x of obj) {
    break;
}
/*
输出:
next() was called
return() was called
*/

for (let x of obj) {
    throw new Error();
}
/*
输出:
next() was called
return() was called
*/


五、for-of循环

在ES6之前,遍历一个数组,最早采用的方式是:

for (var i=0; i<=arr.length; i++) { /* 使用arr[i] */ }

也有人用for-in,如:

for (var i in arr) { /* 使用arr[i] */ }

但是这两种方式都只能获取键名(在数组中称为下标),而不能直接获得值,所以ES5提供了forEach(),可以有:

arr.forEach(function(x) {
    /* 使用x */
});

而ES6里,则更方便,原生提供了for-of这种语句结构,所以可以:

for (let x of arr) {
    /* 使用x */
}

for-of实际上,调用的是[Symbol.iterator](),所以,只要一个对象实现了iterator接口,那么它就能够使用for-of
需要注意的是:数组中,for-of只遍历具有数字索引的属性,而忽略其他非数组索引的属性,而for-in则不忽略,如:

const arr = [1, 2, 3];
arr.foo = 'foo';
for (let x of arr) {
    console.log(x);
}
/* 输出:1 2 3 */
for (let i in arr) {
    console.log(arr[i]);
}
/* 输出:1 2 3 foo */

在ES6里,由于ArraySetMapStringTypedArrayarguments对象NodeList对象都原生实现了iterator接口,所以它们都可以使用for-of遍历,而对象原生是没有实现for-of的,所以不能遍历,如:

const obj = { a: 1, b: 2, c: 3 };
for (let x of obj) {
    console.log(x);
}
// TypeError: obj is not iterable

为此,如果我们想遍历对象的话,那么我们可以这么实现:

function* entries(obj) {
    for (let key of Object.keys(obj)) {
        yield [key, obj[key]];
    }
}

for (let [key, val] of entries(obj)) {
    console.log(key, val);
}
/*
输出:
a 1
b 2
c 3
*/