JavaScript 迭代器和生成器

ECMAScript 6 规范新增的两个高级特性:生成器和迭代器能够使我们高效、清晰、方便的实现迭代。

ECMAScript 较早的版本中,执行迭代必须使用循环或其他辅助结构。随着代码量增加,代码会变得越发混乱。很多语言都通过原生语言结构解决了这个问题,开发者无须事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式。PythonJavaC++,还有其他很多语言都对这个模式提供了完备的支持。

生成器和生成器对象

生成器是 ECMAScript 6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。

需要注意的是:箭头函数不能用来定义生成器函数。(箭头函数的作用域根据当前环境决定)

通过生成器函数可以生成一个遵循可迭代协议(Iterable Protocol)和迭代器协议(Iterator Protocol)的生成器对象。

  • 可迭代协议是指:支持迭代的自我识别能力和创建实现 Iterator 接口的对象的能力。在 ECMAScript 中,这意味着必须暴露一个属性作为 “默认迭代器”,而且这个属性必须使用特殊的 [Symbol.iterator] 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
  • 迭代器协议是指:含有 next() 方法可以在可迭代对象中遍历数据,并返回 { done: Boolean, value: any } 形式的返回数据,done 用来标识迭代是否完成。

yield 关键字可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到 yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next() 方法来恢复执行:

1
2
3
4
5
6
7
8
9
10
11
12
function* generatorFun() {
yield 1;
yield 2;
yield 3;
}

const generatorObj = generatorFun(); // {}

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

生成器对象遵循可迭代协议和迭代器协议,因此它可以通过 for...of 进行迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* generatorFun() {
yield 1;
yield 2;
yield 3;
}

const generatorObj = generatorFun();

for (const iterator of generatorObj) {
console.log(iterator);
}

// 1
// 2
// 3

也可以通过 spread 语法输出其值

1
2
3
4
5
6
7
8
9
function* generatorFun() {
yield 1;
yield 2;
yield 3;
}

const generatorObj = generatorFun();

console.log(...generatorObj); // 1 2 3

通过 yield* 可以将执行委托给另一个迭代器,是将一个生成器的流插入到另一个生成的流的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* generateSquence(start, end) {
for (let i = start; i <= end; i++) yield i;
}

function* generatePwdChars() {
yield* generateSquence(48, 57);
yield* generateSquence(65, 90);
yield* generateSquence(97, 122);
}

let pwdChars = '';
for (const char of generatePwdChars()) {
pwdChars += String.fromCharCode(char);
}

console.log(pwdChars); // 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

yield 不仅可以向外返回结果,还可以将外部的值传递到生成器内部(第一次调用 next() 方法传递的参数会被忽略)

1
2
3
4
5
6
7
8
9
10
11
12
function* gen() {
// 向外部代码传递一个问题并等待答案
let result = yield "2 + 2 = ?"; // (*)

alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield 返回的 value

generator.next(4); // --> 将结果传递到 generator 中

可以通过 generator.throw() 向生成器内部传递一个错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)

console.log("The execution does not reach here, because the exception is thrown above");
} catch(e) {
console.log(e); // Error: The answer is not found in my database
}
}

let generator = gen();

let question = generator.next().value;

generator.throw(new Error("The answer is not found in my database")); // (2)

当迭代值是以异步的形式出现时,就需要使用异步迭代,通过为对象添加 [Symbol.asyncIterator] 方法返回异步迭代器对象,异步迭代器对象通过 for await...of 循环来访问迭代值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let range = {
from: 1,
to: 5,

[Symbol.asyncIterator]: async function* () {
for (let start = this.from; start <= this.to; start++) {
value = awiat doSth();
yield value;
}
}
};

(async () => {
for await (let value of range) { // (4)
alert(value); // 1,2,3,4,5
}
})()

迭代器

JavaScript 中的可迭代对象是对数组的泛化,可以通过 for...of 对其进行迭代。

迭代器是一种一次性使用的对象,用于迭代和它关联的可迭代对象。

Symbol.iterator

可以通过为对象添加一个返回迭代器的 [Symbol.iterator] 方法来使得对象成为可迭代的 – 迭代器是指一个拥有 next() 方法的对象。

通过 for...of 对可迭代对象进行迭代时,for...of 希望获得下一个迭代值时就会调用迭代器对象的 next() 方法,next() 方法返回的对象必须是{ value: any: done: Boolean } 形式的值,当 donetrue 时表示迭代结束,否则表示还可以迭代下一个值。

一般来说,迭代器对象和要迭代的对象是不同的,这样可以避免迭代时造成要迭代对象的属性变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const range = {
from: 1,
to: 3,
[Symbol.iterator]: function () {
return {
current: this.from,
last: this.to,
next() {
if (this.current <= this.last) {
return { value: this.current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};

// for...of 希望获得下一个迭代值时就会调用迭代器对象的 next() 方法
// next() 方法返回的对象必须是 { value: any: done: boolean } 形式的值
for (const iterator of range) {
console.log(iterator);
}
// 1
// 2
// 3

使用 for...of 循环对对象进行迭代时,它会寻找 [Symbol.iterator] 方法,如果没有找到则会报错。

1
2
3
4
5
6
7
8
const range = {
from: 1,
to: 3,
};

for (const iterator of range) { // TypeError: range is not iterable
console.log(iterator);
}

如果需要在迭代过程中做更多的事情,我们可以通过显示调用迭代器来获得比 for...of 跟多的控制权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const range = {
from: 1,
to: 3,
[Symbol.iterator]: function () {
return {
current: this.from,
last: this.to,
next() {
if (this.current <= this.last) {
return { value: this.current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};

const generatorObj = range[Symbol.iterator]();

while(true) {
const res = generatorObj.next();
if (res.done) break;
console.log(res.value);
}

JavaScript 中的内建可迭代对象

JavaScript 中很多内建对象都遵循了可迭代协议:

  • 字符串

  • 数组

  • 映射(Map/WeakMap)

  • 集合(Set/WeakSet)

  • arguments 对象

  • NodeListDOM 集合类型

数组和字符串

数组和字符串是使用最广泛的内建可迭代对象。

1
2
3
4
5
6
7
8
9
const string = 'Best wishes for the beautiful world.'
for (let iterator of string) {
console.log(iterator);
}

const arr = [ 'Best', 'wishes', 'for', 'the', 'beautiful', 'world' ];
for (const iterator of arr) {
console.log(iterator);
}

类数组对象和可迭代对象

JavaScript 中的类数组对象是指拥有 length 属性和索引的对象,但它不一定是可迭代的。

1
2
3
4
5
6
7
const arrLikeObj = {
length: 2,
0: '1',
1: '2'
}

for (const iterator of arrLikeObj) {} // TypeError: arrLikeObj is not iterable

JavaScript 中的可迭代对象是指实现了 [Symbol.iterator] 方法的对象。

我们可以通过全局方法 Array.from 将类数组对象转变成真正的数组再进行迭代

1
2
3
4
5
6
7
8
9
const arrLikeObj = {
length: 2,
0: '1',
1: '2'
}

for (const iterator of Array.from(arrLikeObj)) {
console.log(iterator);
}

使用生成器进行迭代

根据上文对生成器的介绍,很容易想到可以使用生成器函数来返回可迭代对象实现对象的迭代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const range = {
from: 1,
to: 3,
[Symbol.iterator]: function* () {
for (let value = this.from; value <= this.to; value++) {
yield value;
}
}
};

for (const iterator of range) {
console.log(iterator);
}

// 1
// 2
// 3

提前终止迭代器

可选的 return() 方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以 “关闭” 迭代器。可能的情况包括:

  • for...of 循环通过 breakcontinuereturnthrow 提前退出
  • 解构操作并未消费所有值
作者

Y2hlbmdsZWk=

发布于

2019-06-11

更新于

2021-09-01

许可协议