Skip to content

深入理解 ES6 #8-迭代器(Iterator)和生成器(Generator)

🏷️ 《深入理解 ES6》

循环语句的问题

js
var colors = ["red", "green", "blue"];
for (var i = 0, len = colors.length; i < len; i++) {
    console.log(colors[i]);
}

标准的 for 循环代码。虽然也算是简单,但是当循环多层嵌套的时候,则需要跟踪多个循环变量,不小心错误使用了其它循环的跟踪变量,则会导致程序出错。

比如常见的外层 for 循环使用 i 作为循环的索引,内层的 for 循环也使用了同名变量 i 作为循环的索引,运行时就会导致代码报错。

迭代器的出现旨在消除这种复杂性并减少循环中的错误。

什么是迭代器

迭代器是一种特殊的对象,它具有一些专门为迭代过程设计的专有接口,所有的迭代器对象都有一个 next() 方法,每次调用都返回一个结果对象。

结果对象有两个属性:

  1. value 下一个将要返回的值

  2. done 一个布尔型的值

    当没有更多可返回数据时返回 true

如果在最后一个值返回后再调用 next() 方法,结果对象中的 donefalse;属性 value 则包含迭代器最终返回的值,这个返回值不是数据集合的一部分,它与函数的返回值类似,是函数调用过程中最后一次给调用者传递信息的方法,如果没有相关数据则返回 undefined

使用 ES5 的语法创建一个迭代器:

js
function createIterator(items) {
    var i = 0;
    return {
        next: function() {
            var done = (i >= items.length);
            var value = !done ? items[i++] : undefined;

            return {
                done: done,
                value: value
            };
        }
    };
}

var iterator = createIterator([1, 2, 3]);

console.log(iterator.next()); // {done: false, value: 1}
console.log(iterator.next()); // {done: false, value: 2}
console.log(iterator.next()); // {done: false, value: 3}
console.log(iterator.next()); // {done: true, value: undefined}
// 之后所有的调用都返回相同的内容
console.log(iterator.next()); // {done: true, value: undefined}

ES6 中引入了一个生成器对象,它可以让创建迭代器的过程变得简单。

什么是生成器

生成器是一种返回迭代器的函数,通过 function 关键字后的星号(*)来表示,函数中会用到新的关键字 yield。星号可以紧挨着 function 关键字,也可以在中间添加一个空格。

js
// 生成器
function *createIterator() {
    yield 1;
    yield 2;
    yield 3;
}

let iterator = createIterator();

console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

每当执行完一条 yield 语句后函数就会自动停止执行。

使用 yield 关键字可以返回任何值或表达式。

js
// 生成器
function *createIterator(items) {
    for(let i = 0; i < items.length; i++) {
        yield items[i];
    }
}

let iterator = createIterator([1, 2,3]);

console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

生成器函数表达式

只需在 function 关键字和小括号中间添加一个星号即可。

js
let createIterator = function *(items) {
    for(let i = 0; i < items.length; i++) {
        yield items[i];
    }
}

let iterator = createIterator([1, 2,3]);

生成器对象的方法

js
let o = {
    createIterator: function *(items) {
        for(let i = 0; i < items.length; i++) {
            yield items[i];
        }
    }
};

let iterator = o.createIterator([1, 2, 3]);

也可以使用简写方法创建生成器

js
let o = {
    *createIterator(items) {
        for(let i = 0; i < items.length; i++) {
            yield items[i];
        }
    }
};

let iterator = o.createIterator([1, 2, 3]);

可迭代对象和 for-of 循环

可迭代对象具有 Symbol.iterator 属性,是一种与迭代器密切相关的对象。

Symbol.iterator 通过指定的函数可以返回一个作用于附属对象的迭代器。

在 ES6 中,所有集合对象(数组、Set 集合及 Map 集合)和字符串都是可迭代对象,这些对象中都有默认的迭代器。

ES 中新加入的特性 for-of 循环需要用到可迭代对象的这些功能。

js
let values = [1, 2, 3];

for (let num of values) {
    console.log(num);
}

这段代码的输出

js
1
2
3

for-of 循环通过调用 values 数组的 Symbol.iterator 方法来获取迭代器;

随后迭代器的 next() 方法被多次调用,从其返回对象的 value 属性读取值并存储在变量 num 中;

当结果对象的 done 属性为 true 时退出循环,所有 num 不会被赋值为 undefined

访问默认迭代器

可以通过 Symbol.iterator 来访问对象的默认迭代器。

js
let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();

console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

使用 Symbol.iterator 来检测对象是否是可迭代对象:

js
function isIterable(object) {
    return typeof object[Symbol.iterator] === "function";
}

console.log(isIterable([1, 2, 3])); // true
console.log(isIterable("Hello")); // true
console.log(isIterable(new Map())); // true
console.log(isIterable(new Set())); // true
console.log(isIterable(new WeakMap())); // false
console.log(isIterable(new WeakSet())); // false

创建可迭代对象

默认情况下开发者定义的对象都是不可迭代的,但如果给 Symbol.iterator 属性添加一个生成器,则可以将其变为可迭代对象。

js
let collection = {
    items: [],
    *[Symbol.iterator]() {
        for (let item of this.items) {
            yield item;
        }
    }
};

collection.items.push(1);
collection.items.push(2);
collection.items.push(3);

for (let x of collection) {
    console.log(x);
}

这段代码输出以下内容:

js
1
2
3

内建迭代器

集合对象迭代器

ES6 有三种类型的集合对象:数组、Map 集合和 Set 集合,这 3 中对象都内建了以下三种迭代器:

  1. entries() 返回一个迭代器,其值为多个键值对;

  2. values() 返回一个迭代器,其值为集合的值(后面在运行示例时发现,数组没有该方法,但存在相同功能的迭代器);

  3. keys() 返回一个迭代器,其值为集合中的所有键名。

entries() 迭代器

返回一个数组,数组中两个元素分别表示为每个元素的键和值。

遍历对象是数组时,键是数字类型的索引;Set 集合时,键同值一样;Map 集合时,第一个元素为键名,第二个元素为值。

js
let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

for (let entry of colors.entries()) {
    console.log(entry);
}

for (let entry of tracking.entries()) {
    console.log(entry);
}

for (let entry of data.entries()) {
    console.log(entry);
}

上述代码执行结果如下:

js
[0, "red"]
[1, "green"]
[2, "blue"]
[1234, 1234]
[5678, 5678]
[9012, 9012]
["title", "Understanding ECMAScript 6"]
["format", "ebook"]

values() 迭代器

返回集合中所存的所有值。

js
let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

for (let value of colors) {
    console.log(value);
}

for (let value of tracking.values()) {
    console.log(value);
}

for (let value of data.values()) {
    console.log(value);
}

上述代码执行结果如下:

js
red
green
blue
1234
5678
9012
Understanding ECMAScript 6
ebook

关于数组的 values() 迭代器,书上示例的写法是 let value of colors.values(),但实际运行时出错了。

Uncaught TypeError: colors.values(...) is not iterable

但实际上该迭代器还是存在的,可以通过 colors[Symbol.iterator] 查看:

js
ƒ values() { [native code] }

因为数组的默认迭代器即为 values() 迭代器,所以可以省略 values(),直接使用变量本身 let value of colors 即可。

keys() 迭代器

返回集合中存在的每一个键。

数组类型返回数字类型的键;Set 类型因为键和值相同,所有返回的也是值;Map 类型返回的是所有的键。

js
let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

for (let key of colors.keys()) {
    console.log(key);
}

for (let key of tracking.keys()) {
    console.log(key);
}

for (let key of data.keys()) {
    console.log(key);
}

执行结果:

js
0
1
2
1234
5678
9012
title
format

不同集合类型的默认迭代器

每个集合都有一个默认的迭代器,在 for-of 循环中,如果没有显示指定迭代器,则使用默认的迭代器。

数组和 Set 集合的默认迭代器是 values() 方法;Map 集合的默认迭代器是 entries() 方法。

js
let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

for (let key of colors) {
    console.log(key);
}

for (let key of tracking) {
    console.log(key);
}

for (let key of data) {
    console.log(key);
}

执行结果:

js
red
green
blue
1234
5678
9012
["title", "Understanding ECMAScript 6"]
["format", "ebook"]

字符串迭代器

“𠮷”是双字节字符,由于方括号操作的是编码单元而非字符,所以无法正确访问双字节字符。

js
var message = "A 𠮷 B";
for (let i = 0; i < message.length; i++) {
    console.log(message[i]);
}

输出结果如下:

js
A
 


 
B

在 ES6 中,通过字符串默认的迭代器,可以正确的获取每个字符的内容。

js
var message = "A 𠮷 B";
for (let c of message) {
    console.log(c);
}

输出结果:

js
A
 
𠮷
 
B

NodeList 迭代器

DOM 标准中有一个 NodeList 类型,document 对象中的所有元素都用这个类型来表示。

ES6 添加了默认迭代器之后,DOM 定义中的 NodeList 类型也拥有了默认迭代器,其行为与数组的默认迭代器完全一致。

js
var divs = document.getElementsByTagName("div");

for (let div of divs) {
    console.log(div.id);
}

展开运算符与非数组可迭代对象

js
let set = new Set([1, 2, 3, 3, 3, 3, 4, 5]),
    array = [...set];
console.log(array); // [1, 2, 3, 4, 5]

展开运算符可以操作所有的可迭代对象,并根据默认迭代器来选取要引用的值,从迭代器中读取所有的值。

高级迭代器功能

给迭代器传递参数

如果给迭代器的 next() 方法传递参数,则这个参数的值就会替代生成器内部上一条 yield 语句的返回值。

js
function *createIterator() {
    let first = yield 1;
    let second = yield first + 2;
    yield second + 3;
}

let iterator = createIterator();

console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next(4)); // {value: 6, done: false}
console.log(iterator.next(5)); // {value: 8, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

在迭代器中抛出错误

js
function *createIterator() {
    let first = yield 1;
    let second = yield first + 2;
    yield second + 3;
}

let iterator = createIterator();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next(4)); // {value: 6, done: false}
console.log(iterator.throw(new Error("Boom"))); // 从生成器中抛出的错误
// VM50:4 Uncaught Error: Boom

可以在生成器内部通过 try-catch 捕获这些异常。

js
function *createIterator() {
    let first = yield 1;
    let second;
    
    try {
        second = yield first + 2;
    } catch (ex) {
        second = 6;
    }
    
    yield second + 3;
}

let iterator = createIterator();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next(4)); // {value: 6, done: false}
console.log(iterator.throw(new Error("Boom"))); // {value: 9, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

上面的代码中,调用 throw() 方法后也会像调用 next() 方法一样返回了结果对象。

生成器返回语句

生成器也是函数,可以通过 return 语句提前退出函数。

js
function *createIterator() {
    yield 1;
    return;
    yield 2;
    yield 3;
}

let iterator = createIterator();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

return 语句也可以指定返回值,该值将被赋值给返回对象的 value 属性。

js
function *createIterator() {
    yield 1;
    return 32;
}

let iterator = createIterator();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 32, done: true}
console.log(iterator.next()); // {value: undefined, done: true}

NOTE

展开运算符与 for-of 循环语句会直接忽略通过 return 语句指定的任何返回值,只要 done 一变为 true 就立即停止读取其它的值。

委托生成器

通过给 yield 语句添加一个星号,就可以将生成数据的过程委托给其它生成器。

js
function *createNumberIterator() {
    yield 1;
    yield 2;
}

function *createColorIterator() {
    yield "red";
    yield "green";
}

function *createCombinedIterator() {
    yield *createNumberIterator();
    yield *createColorIterator();
    yield true;
}

var iterator = createCombinedIterator();

console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: "red", done: false}
console.log(iterator.next()); // {value: "green", done: false}
console.log(iterator.next()); // {value: true, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

可以使用委托的生成器的返回值来处理复杂任务。

js
function *createNumberIterator() {
    yield 1;
    yield 2;
    return 3;
}

function *createRepeatingIterator(count) {
    for (let i = 0; i < count; i++) {
        yield "repeat";
    }
}

function *createCombinedIterator() {
    let result = yield *createNumberIterator();
    yield *createRepeatingIterator(result);
}

var iterator = createCombinedIterator();

console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: "repeat", done: false}
console.log(iterator.next()); // {value: "repeat", done: false}
console.log(iterator.next()); // {value: "repeat", done: false}
console.log(iterator.next()); // {value: undefined, done: true}

NOTE

yield * 也可直接应用于字符串,例如 yield *"Hello",此时将使用字符串的默认迭代器。