0%

JS高程第4版新增章节翻译:迭代器和生成器

下述内容翻译自 Professional JavaScript for Web Developes,4th Edition(JavaScript 高级程序设计第四版),225 页,Iterators and Generators(迭代器和生成器) 章节。

迭代器和生成器

术语“迭代(iteration)”来源于拉丁文 itero,意思是“重复(repeat)”或“再做一次(do again)”。在上下文中,“迭代”意味着按顺序重复执行一个过程,并按照期望停止。ECMAScript6 规范引入了两个高级特性——迭代器(iterator)和生成器(generator)——以实现更简洁、快速和容易的迭代过程。

迭代介绍

在 JavaScript 中,一个最简单的迭代例子就是循环计数(counting loop):

1
2
3
for (let i = 1; i <= 10; ++i) {
console.log(i);
}

循环(loop)是一个基本的迭代工具,因为它们允许指定应该进行多少次迭代以及在每次迭代中应该发生什么。每个循环迭代将在下一个迭代开始之前执行完成,并且每个迭代发生的前后顺序是明确的。

迭代可以在有内容的有序集合(ordered collections)上进行。“有序(ordered)”意味着有一个可接受的顺序,按照此顺序,所有项都应该被遍历,并且有一个确定的开始项和结束项。在 JavaScript 中,最常见的有序集合的例子是数组(Array)。

1
2
3
4
let collection = ['foo', 'bar', 'baz'];
for (let index = 0; index < collection.length; ++index) {
console.log(collection[index]);
}

由于数组长度已知,并且可以通过数组的索引检索该数组中的每一项,因此可以通过索引递增来顺序遍历整个数组。

这种循环中的基本流程并不理想,原因如下:

  • 遍历数据结构需要具备如何使用数据结构的特定知识。只能通过首先引用数组对象,然后使用[]操作符在特定索引处检索数组中的该项。结果就是性能没有优化。
  • 遍历顺序并不是数据结构固有的属性。使用递增整数访问索引是特定于数组类型的,并不广泛应用到具有隐式排序的其他数据结构。

ES5 引入了 Array.prototype.forEach 方法,更接近于所需要的功能(但仍然不是理想的解决方案):

1
2
3
4
5
let collection = ['foo', 'bar', 'baz'];
collection.forEach((item) => console.log(item));
// foo
// bar
// baz

forEach 方法解决了通过数组对象既需要跟踪索引又需要检索该索引对应项的问题。但是,迭代无法中止,而且仅能应用于数组,回调函数也不易用。

对于早期版本的 ECMAScript,执行迭代需要使用循环等辅助构造,随着代码复杂度的增加,这是一个越来越麻烦的事情。许多语言已经使用原生语言结构解决了这个问题,这种结构允许在具体了解迭代实际上是如何发生的情况下执行一种解决方案:迭代器模式(iterator pattern)。Python、Java、C++和许多其他语言,还有 ES6 规范的 JavaScript,都为此模式提供了完善的支持。

迭代器模式

迭代器模式(在 ECMAScript 的上下文环境中特指)描述了一种解决方案,其中一些东西可以被描述为“可迭代”的,并且可以实现一个由迭代器(Iterator)使用的形式化的可迭代对象(Iterable)接口。

“可迭代”的概念是抽象的。通常情况下,迭代会采用一个集合对象的形式,比如数组或 set,这两种对象都有有限数量的可数元素,并且遍历顺序明确:

1
2
3
4
5
6
// 数组具有有限数量的可数元素
// 按照递增索引顺序遍历每个索引
let arr = [3, 1, 4];
// Set具有有限数量的可数元素
// 按照插入顺序遍历每个索引
let set = new Set().add(3).add(1).add(4);

但是,迭代器不是必须链接到集合对象。它还可以链接到某个具有类数组行为的东西上,例如本章前文提到的循环计数。这个循环中生成的值是暂时的,但是这样的循环正在执行迭代。这个循环计数和数组都可以表现为可迭代对象。

瞬态迭代可以用生成器(generator)实现,这将在本章后文讨论。

任何实现了迭代器接口的对象都可以“使用”实现了可迭代接口的对象。迭代器是根据需求创建的单独对象且只有单一用途。每个迭代器都与一个可迭代对象相关联,迭代器公开一个 API 便于使用相关的可迭代对象。迭代器不需要理解与之关联的可迭代对象的结构;它只需要知道如何检索顺序值。关注点分离就是使得 可迭代对象/迭代器(Iterable/Iterator) 约定如此有用的原因。

可迭代对象协议

实现可迭代对象接口既需要自我标识为支持迭代,也需要创建实现迭代器接口的对象。在 ECMAScript 中,这意味着必须公开一个属性:即用特殊符号 Symbol.iterator 标识的“默认迭代器”。这个默认迭代器属性必须引用一个迭代器工厂函数,该函数在调用时将生成一个新的迭代器。

许多内置类型实现了迭代接口:

  • Strings
  • Arrays
  • Maps
  • Sets
  • arguments 对象
  • 一些 DOM 集合类型(比如 NodeList)

检查默认迭代器属性中是否为工厂函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let num = 1;
let obj = {};
// 这些类型没有迭代器工厂函数
console.log(num[Symbol.iterator]); // undefined
console.log(obj[Symbol.iterator]); // undefined
let str = 'abc';
let arr = ['a', 'b', 'c'];
let map = new Map().set('a', 1).set('b', 2).set('c', 3);
let set = new Set().add('a').add('b').add('c');
let els = document.querySelectorAll('div');
// 这些类型全都有迭代器工厂函数
console.log(str[Symbol.iterator]); // f values() { [native code] }
console.log(arr[Symbol.iterator]); // f values() { [native code] }
console.log(map[Symbol.iterator]); // f values() { [native code] }
console.log(set[Symbol.iterator]); // f values() { [native code] }
console.log(els[Symbol.iterator]); // f values() { [native code] }
// 调用工厂函数生成一个迭代器
console.log(str[Symbol.iterator]()); // StringIterator {}
console.log(arr[Symbol.iterator]()); // ArrayIterator {}
console.log(map[Symbol.iterator]()); // MapIterator {}
console.log(set[Symbol.iterator]()); // SetIterator {}
console.log(els[Symbol.iterator]()); // ArrayIterator {}

不一定需要显式地调用工厂函数来生成迭代器。任何实现可迭代协议的对象都自动与迭代器语言特性兼容。这些原生语言结构包括:

  • for… of 循环
  • 数组解构
  • 扩展操作符
  • Arry.from ()
  • Set 结构
  • Map 结构
  • Promise.all () ,表示一个可迭代的 promise
  • Promise.race () ,表示一个可迭代的 promise
  • yield* 操作符,用于生成器

这些本地语言结构自动调用迭代器工厂函数来创建一个迭代器:

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
let arr = ['foo', 'bar', 'baz'];
// for...of 循环
for (let el of arr) {
console.log(el);
}
// foo
// bar
// baz
// Array 解构
let [a, b, c] = arr;
console.log(a, b, c); // foo, bar, baz
// 扩展操作符
let arr2 = [...arr];
console.log(arr2); // ['foo', 'bar', 'baz']
// Array.from()
let arr3 = Array.from(arr);
console.log(arr3); // ['foo', 'bar', 'baz']
// Set 结构
let set = new Set(arr);
console.log(set); // Set(3) {'foo', 'bar', 'baz'}
// Map 结构
let pairs = arr.map((x, i) => [x, i]);
console.log(pairs); // [['foo', 0], ['bar', 1], ['baz', 2]]
let map = new Map(pairs);
console.log(map); // Map(3) { 'foo'=>0, 'bar'=>1, 'baz'=>2 }

如果一个对象的原型链上的父类实现了可迭代对象接口,那么该对象也同样实现该接口:

1
2
3
4
5
6
7
8
class FooArray extends Array {}
let fooArr = new FooArray('foo', 'bar', 'baz');
for (let el of fooArr) {
console.log(el);
}
// foo
// bar
// baz

迭代器协议

迭代器是一个一次性使用的对象,它将遍历与之关联的任何迭代器对象。迭代器 APi 使用 next()方法在迭代过程中前进。每次调用 next()时,都将返回包含迭代器中下一个值的 迭代器结果(IteratorResult) 对象。如果不调用 next()方法,就无法知道迭代器的当前位置。

next()方法返回一个具有两个属性的对象:done,一个布尔值,指示是否可以再次调用 next()来检索更多的值;value,包含可迭代对象下一个值,或者 undefined(如果 done 为 true)。done: true 的状态称为“耗竭(exhaustion)”,可以用一个简单的数组来演示:

1
2
3
4
5
6
7
8
9
10
11
// 可迭代对象
let arr = ['foo', 'bar'];
// 迭代器工厂函数
console.log(arr[Symbol.iterator]); // f values() { [native code] }
// 迭代器
let iter = arr[Symbol.iterator]();
console.log(iter); // ArrayIterator {}
// 执行迭代过程
console.log(iter.next()); // { done: false, value: 'foo' }
console.log(iter.next()); // { done: false, value: 'bar' }
console.log(iter.next()); // { done: true, value: undefined }

通过创建迭代器和调用 next(),数组按顺序进行迭代,直到停止生成新值为止。注意,迭代器不知道如何在可迭代对象中检索下一个值,也不知道可迭代对象有多大。一旦迭代器达到 done:true 状态,再次调用 next()的结果是幂等的:

1
2
3
4
5
6
let arr = ['foo'];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // { done: false, value: 'foo' }
console.log(iter.next()); // { done: true, value: undefined }
console.log(iter.next()); // { done: true, value: undefined }
console.log(iter.next()); // { done: true, value: undefined }

每个迭代器表示可迭代对象的一次有序遍历。一个迭代器实例不知道其他迭代器实例的存在,它们都将独立遍历迭代:

1
2
3
4
5
6
7
let arr = ['foo', 'bar'];
let iter1 = arr[Symbol.iterator]();
let iter2 = arr[Symbol.iterator]();
console.log(iter1.next()); // { done: false, value: 'foo' }
console.log(iter2.next()); // { done: false, value: 'foo' }
console.log(iter2.next()); // { done: false, value: 'bar' }
console.log(iter1.next()); // { done: false, value: 'bar' }

迭代器不绑定到可迭代对象的快照;它仅仅使用游标(cursor)在可迭代对象中跟踪迭代进度。如果迭代过程中可迭代对象发生了更改,迭代器将合并这些变化:

1
2
3
4
5
6
7
8
let arr = ['foo', 'baz'];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // { done: false, value: 'foo' }
// 在数组的中间插入一个值
arr.splice(1, 0, 'bar');
console.log(iter.next()); // { done: false, value: 'bar' }
console.log(iter.next()); // { done: false, value: 'baz' }
console.log(iter.next()); // { done: true, value: undefined }

注意:迭代器保持对可迭代对象的持续引用,因此迭代器的存在将防止可迭代对象的垃圾收集过程。

术语“迭代器(iterator)”的概念可能有些模糊,因为指的是一个广义的迭代概念、接口和形式化的迭代器类。下面的例子比较了显式的迭代器实现和原生的迭代器实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 这个类实现了可迭代对象的接口
// 调用默认的迭代器工厂函数将会返回一个实现了接口的迭代器对象
class Foo {
[Symbol.iterator]() {
return {
next() {
return { done: false, value: 'foo' };
},
};
}
}
let f = new Foo();
// 打印实现了迭代器接口的对象
console.log(f[Symbol.iterator]()); // { next: f() {} }
// 这个数组类型实现了迭代器接口
// 调用数组类型默认的迭代器将会创建一个ArrayIterator实例对象
let a = new Array();
// 打印ArrayIterator实例对象
console.log(a[Symbol.iterator]()); // Array Iterator {}

自定义迭代器

与可迭代对象接口一样,任何实现迭代器接口的对象都可以作为迭代器来使用。参考下面的示例,其中定义了 Counter 类来迭代指定的次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Counter {
// Counter实例应该迭代<limit>次
constructor(limit) {
this.count = 1;
this.limit = limit;
}
next() {
if (this.count <= this.limit) {
return { done: false, value: this.count++ };
} else {
return { done: true, value: undefined };
}
}
[Symbol.iterator]() {
return this;
}
}
let counter = new Counter(3);
for (let i of counter) {
console.log(i);
}
// 1
// 2
// 3

满足了迭代器接口的要求,但是这不是最优实现,因为每个类的实例只能迭代一次:

1
2
3
4
5
6
7
8
9
10
for (let i of counter) {
console.log(i);
}
// 1
// 2
// 3
for (let i of counter) {
console.log(i);
}
// (nothing logged)

为了允许从单个可迭代对象创建多个迭代器,count 必须是在每个迭代器的基础上创建。为了解决这个问题,可以返回一个通过闭包来使用 count 变量的迭代器对象,:

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
27
28
29
30
31
class Counter {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
let count = 1,
limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
},
};
}
}
let counter = new Counter(3);
for (let i of counter) {
console.log(i);
}
// 1
// 2
// 3
for (let i of counter) {
console.log(i);
}
// 1
// 2
// 3

以这种方式创建的每个迭代器也都实现了迭代接口。Symbol.iterator 属性指的是返回相同迭代器的工厂:

1
2
3
4
5
let arr = ['foo', 'bar', 'baz'];
let iter1 = arr[Symbol.iterator]();
console.log(iter1[Symbol.iterator]); // f values() { [native code] }
let iter2 = iter1[Symbol.iterator]();
console.log(iter1 === iter2); // true

因为每个迭代器也都实现了可迭代对象接口,所以它们可以在任何需要迭代的地方使用,例如 for…of 循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let arr = [3, 1, 4];
let iter = arr[Symbol.iterator]();
for (let item of arr) {
console.log(item);
}
// 3
// 1
// 4
for (let item of iter) {
console.log(item);
}
// 3
// 1
// 4

迭代器提前终止

迭代器中有一个可选的 return()方法,该方法允许指定仅当迭代器提前关闭时才执行的行为。当执行迭代的结构希望向迭代器指示,表示不打算完成全部遍历时,就会发生迭代器的关闭。可能发生的情况包括:

  • 一个 for…of 循环通过 break、continue、return 或 throw 提前退出。

  • 解构操作不会使用所有值。

Return()方法必须返回有效的 IteratorResult 对象。一个简单的迭代器实现应该只返回{ done: true },因为返回值只在生成器的上下文中使用,将在本章后文讨论这个问题。

如下面的代码所示,一旦内置语言解构识别出需要迭代且不会被使用的其他值,将自动调用 return()方法。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Counter {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
let count = 1,
limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true };
}
},
return() {
console.log('Exiting early');
return { done: true };
},
};
}
}

let counter1 = new Counter(5);
for (let i of counter1) {
if (i > 2) {
break;
}
console.log(i);
}
// 1
// 2
// Exiting early

let counter2 = new Counter(5);
try {
for (let i of counter2) {
if (i > 2) {
throw 'err';
}
console.log(i);
}
} catch (e) {}
// 1
// 2
// Exiting early
let counter3 = new Counter(5);
let [a, b] = counter3;
// Exiting early

如果迭代器没有关闭,那么可以从中断的地方继续迭代,比如数组的迭代器是不可关闭的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let a = [1, 2, 3, 4, 5];
let iter = a[Symbol.iterator]();
for (let i of iter) {
console.log(i);
if (i > 2) {
break;
}
}
// 1
// 2
// 3
for (let i of iter) {
console.log(i);
}
// 4
// 5

因为 return()方法是可选的,所以并非所有迭代器都可关闭。通过测试迭代器实例的 return 属性是否是函数对象,可以确定迭代器是否可关闭。但是,仅仅将 return 方法添加到一个不可关闭的迭代器中并不会使它变得可关闭,因为调用 return()并不会强制迭代器进入关闭状态。然而,return()方法仍然会被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let a = [1, 2, 3, 4, 5];
let iter = a[Symbol.iterator]();
iter.return = function () {
console.log('Exiting early');
return { done: true };
};
for (let i of iter) {
console.log(i);
if (i > 2) {
break;
}
}
// 1
// 2
// 3
// Exiting early
for (let i of iter) {
console.log(i);
}
// 4
// 5

生成器

生成器(generator)是 ECMAScript6 规范中引入的一种方便灵活的构造,提供了在单个函数块中暂停和恢复代码执行的能力。这种新能力的影响是深远的:这允许定义自定义迭代器和实现程序协同。

生成器基础

生成器采用函数的形式,名称用星号表示。只要函数定义有效,那么生成器函数定义也同样有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 生成器函数的函数式声明
function* generatorFn() {}
// 生成器函数的表达式声明
let generatorFn = function* () {};
// 对象字面量方法中的生成器函数
let foo = {
*generatorFn() {},
};
// 类实例中的生成器函数方法
class Foo {
*generatorFn() {}
}
// 类中的生成器函数静态方法
class Bar {
static *generatorFn() {}
}

注意:箭头函数不能用作生成器函数。

该函数将被视为一个生成器,而不必考虑星号周围的空格:

1
2
3
4
5
6
7
8
9
// 等同的生成器函数:
function* generatorFnA() {}
function *generatorFnB() {}
function * generatorFnC() {}
// 等同的生成器方法:
class Foo {
*generatorFnD() {}
* generatorFnE() {}
}

调用时,生成器函数返回一个生成器对象。生成器对象开始时处于暂停状态。与迭代器一样,这些生成器对象实现了迭代器接口,因此具有 next()方法的特性,该方法在被调用时指示生成器开始或继续执行。

1
2
3
4
function* generatorFn() {}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.next); // f next() { [native code] }

这个 next()方法的返回值与迭代器的返回值相同,也具有 done 和 value 属性。带有空函数体的生成器函数将充当一个直通车(passthrough),调用 next()一次就使生成器到达 done:true 状态。

1
2
3
4
function* generatorFn() {}
let generatorObject = generatorFn();
console.log(generatorObject); // generatorFn {<suspended>}
console.log(generatorObject.next()); // { done: true, value: undefined }

value 属性是生成器函数的返回值,默认为 undefined,可以通过生成器函数的返回值指定。

1
2
3
4
5
6
function* generatorFn() {
return 'foo';
}
let generatorObject = generatorFn();
console.log(generatorObject); // generatorFn {<suspended>}
console.log(generatorObject.next()); // { done: true, value: 'foo' }

生成器函数只在第一次 next()调用时执行,如下所示:

1
2
3
4
5
6
function* generatorFn() {
console.log('foobar');
}
// 当生成器函数初始化调用时还不会打印任何东西
let generatorObject = generatorFn();
generatorObject.next(); // foobar

生成器对象实现了迭代器接口,默认迭代器是自引用的:

1
2
3
4
5
6
7
8
9
10
11
12
function* generatorFn() {}
console.log(generatorFn);
// f* generatorFn() {}
console.log(generatorFn()[Symbol.iterator]);
// f [Symbol.iterator]() {native code}
console.log(generatorFn());
// generatorFn {<suspended>}
console.log(generatorFn()[Symbol.iterator]());
// generatorFn {<suspended>}
const g = generatorFn();
console.log(g === g[Symbol.iterator]());
// true

yield 停止执行

关键字 yield 允许生成器停止和开始执行,使得生成器真正起到了作用。生成器函数将正常执行,直到遇到 yield 关键字时,执行将停止,并保留函数的作用域状态。只有当生成器对象调用 next()方法时,执行才会恢复:

1
2
3
4
5
6
function* generatorFn() {
yield;
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: false, value: undefined }
console.log(generatorObject.next()); // { done: true, value: undefined }

yield 关键字表现为一个中间函数返回,并且可以用 next()方法返回暂停时的值。生成器函数通过关键字 yield 停止时,done 为 false,通过关键字 return 退出时 done 为 true:

1
2
3
4
5
6
7
8
9
function* generatorFn() {
yield 'foo';
yield 'bar';
return 'baz';
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: false, value: 'foo' }
console.log(generatorObject.next()); // { done: false, value: 'bar' }
console.log(generatorObject.next()); // { done: true, value: 'baz' }

生成器函数中的执行进度限定在每个生成器对象实例内部。在一个生成器对象实例上调用 next()不会影响其他对象:

1
2
3
4
5
6
7
8
9
10
11
function* generatorFn() {
yield 'foo';
yield 'bar';
return 'baz';
}
let generatorObject1 = generatorFn();
let generatorObject2 = generatorFn();
console.log(generatorObject1.next()); // { done: false, value: 'foo' }
console.log(generatorObject2.next()); // { done: false, value: 'foo' }
console.log(generatorObject2.next()); // { done: false, value: 'bar' }
console.log(generatorObject1.next()); // { done: false, value: 'bar' }

yield 关键字只能在生成器函数中使用,其他任何地方都会抛出 error。与函数 return 关键字一样,yield 关键字必须直接出现在生成器函数定义中。在非生成器函数中进一步嵌套 yield 会抛出一个语法错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 有效
function* validGeneratorFn() {
yield;
}
// 无效
function* invalidGeneratorFnA() {
function a() {
yield;
}
}
// 无效
function* invalidGeneratorFnB() {
const b = () => {
yield;
};
}
// 无效
function* invalidGeneratorFnC() {
(() => {
yield;
})();
}
将生成器对象作为可迭代对象

很少需要对生成器对象显式地调用 next()。相反,生成器作为迭代使用时更有用,如下所示:

1
2
3
4
5
6
7
8
9
10
11
function* generatorFn() {
yield 1;
yield 2;
yield 3;
}
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3

当需要定义自定义迭代对象时可能会特别有用。例如,定义一个将返回执行特定次数迭代器的可迭代对象很有用。使用生成器,可以简单地通过一个循环来完成:

1
2
3
4
5
6
7
8
9
10
11
function* nTimes(n) {
while (n--) {
yield;
}
}
for (let _ of nTimes(3)) {
console.log('foo');
}
// foo
// foo
// foo

生成器函数的参数控制循环迭代次数。当 n 达到 0 时,while 条件将为假,循环将退出,生成器函数将返回。

使用 yield 输入和输出

关键字 yield 也表现为一个中间函数的参数。生成器上次暂停执行的 yield 关键字将参数传递给 next()的第一个值。令人困惑的是没有使用传递给第一个 next()调用的值,因为 第一个 next()是开始执行生成器函数时调用的:

1
2
3
4
5
6
7
8
9
function* generatorFn(initial) {
console.log(initial);
console.log(yield);
console.log(yield);
}
let generatorObject = generatorFn('foo');
generatorObject.next('bar'); // foo
generatorObject.next('baz'); // baz
generatorObject.next('qux'); // qux

关键字 yield 可以同时用作输入和输出,如下例所示:

1
2
3
4
5
6
function* generatorFn() {
return yield 'foo';
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: false, value: 'foo' }
console.log(generatorObject.next('bar')); // { done: true, value: 'bar' }

因为函数必须对整个表达式求值以确定要返回的值,所以当遇到 yield 关键字时,它将暂停执行,并将 foo 作为 yield 的值。下一个次 next()调用提供 “bar” 值作为 yield 的值,而这又作为生成器函数返回值进行计算。

关键字 yield 不限于单次使用。无限计数的生成器函数可以如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
function* generatorFn() {
for (let i = 0; ; ++i) {
yield i;
}
}
let generatorObject = generatorFn();
console.log(generatorObject.next().value); // 0
console.log(generatorObject.next().value); // 1
console.log(generatorObject.next().value); // 2
console.log(generatorObject.next().value); // 3
console.log(generatorObject.next().value); // 4
console.log(generatorObject.next().value); // 5

假设想要定义一个生成器函数,该函数将迭代可配置的次数并生成索引。这可以通过实例化一个新的数组来实现,但是相同的行为可以在不使用数组的情况下完成:

1
2
3
4
5
6
7
8
9
10
11
function* nTimes(n) {
for (let i = 0; i < n; ++i) {
yield i;
}
}
for (let x of nTimes(3)) {
console.log(x);
}
// 0
// 1
// 2

或者,下面的 while 循环实现更加简练:

1
2
3
4
5
6
7
8
9
10
11
12
function* nTimes(n) {
let i = 0;
while (n--) {
yield i++;
}
}
for (let x of nTimes(3)) {
console.log(x);
}
// 0
// 1
// 2

用上述方式使用生成器,也提供了一种实现范围定义或填充数组的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* range(start, end) {
let i = start;
while (end > start) {
yield start++;
}
}
for (const x of range(4, 7)) {
console.log(x);
}
// 4
// 5
// 6
function* zeroes(n) {
while (n--) {
yield 0;
}
}
console.log(Array.from(zeroes(8))); // [0, 0, 0, 0, 0, 0, 0, 0]
yield 可迭代对象

可以增强 yield 的行为,使其遍历一个可迭代对象并一次 yield 一项。这可以使用星号完成,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// generatorFn和下述代码效果等同:
// function* generatorFn() {
// for (const x of [1, 2, 3]) {
// yield x;
// }
// }
function* generatorFn() {
yield* [1, 2, 3];
}
let generatorObject = generatorFn();
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3

像 生成器函数的星号一样,yield 星号周围的空白不会改变它的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* generatorFn() {
yield* [1, 2];
yield *[3, 4];
yield * [5, 6];
}
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
// 4
// 5
// 6

因为 yield*实际上只是将一个可迭代对象的 yield 值序列化为一系列的 yield 值,所以使用 yield*与将 yield 值放在循环中没有任何区别。下述这两个生成器函数在行为上是等价的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function* generatorFnA() {
for (const x of [1, 2, 3]) {
yield x;
}
}
for (const x of generatorFnA()) {
console.log(x);
}
// 1
// 2
// 3
function* generatorFnB() {
yield* [1, 2, 3];
}
for (const x of generatorFnB()) {
console.log(x);
}
// 1
// 2
// 3

yield *的值是关联迭代器的 done:true 随附的 value 属性。对于普通的迭代器来说,此值是 undefined:

1
2
3
4
5
6
7
8
9
10
function* generatorFn() {
console.log('iter value:', yield* [1, 2, 3]);
}
for (const x of generatorFn()) {
console.log('value:', x);
}
// value: 1
// value: 2
// value: 3
// iter value: undefined

对于由生成器函数产生的迭代器来说,这个值将以生成器函数返回的任何值的形式出现:

1
2
3
4
5
6
7
8
9
10
11
12
function* innerGeneratorFn() {
yield 'foo';
return 'bar';
}
function* outerGeneratorFn(genObj) {
console.log('iter value:', yield* innerGeneratorFn());
}
for (const x of outerGeneratorFn()) {
console.log('value:', x);
}
// value: foo
// iter value: bar
使用 yield*的递归算法

当在递归操作中使用 yield*最为有用,生成器可以产生自身。

参考下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
function* nTimes(n) {
if (n > 0) {
yield* nTimes(n - 1);
yield n - 1;
}
}
for (const x of nTimes(3)) {
console.log(x);
}
// 0
// 1
// 2

在这个示例中,每个生成器首先 yield 来自新创建的生成器对象的每个值,然后生成单个整数。结果是生成器函数将递归递减计数器值并实例化另一个生成器对象,该对象在顶层将具有创建返回增量整数的单个可迭代对象的作用。

使用递归生成器结构和 yield*允许优雅地表示递归算法。参考下面的图实现,它生成一个随机的双向图:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Node {
constructor(id) {
this.id = id;
this.neighbors = new Set();
}
connect(node) {
if (node !== this) {
this.neighbors.add(node);
node.neighbors.add(this);
}
}
}
class RandomGraph {
constructor(size) {
this.nodes = new Set();
// 创建 nodes
for (let i = 0; i < size; ++i) {
this.nodes.add(new Node(i));
}
// 随机链接nodes
const threshold = 1 / size;
for (const x of this.nodes) {
for (const y of this.nodes) {
if (Math.random() < threshold) {
x.connect(y);
}
}
}
}
// 仅用于调试目的
print() {
for (const node of this.nodes) {
const ids = [...node.neighbors].map((n) => n.id).join(',');
console.log('${node.id}: ${ids}');
}
}
}
const g = new RandomGraph(6);
g.print();
// 实例输出:
// 0: 2,3,5
// 1: 2,3,4,5
// 2: 1,3
// 3: 0,1,2,4
// 4: 2,3
// 5: 0,4

图数据结构非常适合于递归遍历,并且使用递归生成器可以完全做到这一点。为此,生成器函数必须接受一个可迭代对象,产生该对象中的每个值,然后递归每个值。一个简单的用途就是测试图的连接,这意味着没有节点无法到达。可以通过从一个节点开始并尝试访问每个节点来完成此测试。下面实例是深度优先遍历的非常简洁的实现:

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
27
28
29
30
31
32
33
34
class Node {
constructor(id) {
// ...
}
connect(node) {
// ...
}
}
class RandomGraph {
constructor(size) {
// ...
}
print() {
// ...
}
isConnected() {
const visitedNodes = new Set();
function* traverse(nodes) {
for (const node of nodes) {
if (!visitedNodes.has(node)) {
yield node;
yield* traverse(node.neighbors);
}
}
}
// 抓住Set中的第一个node
const firstNode = this.nodes[Symbol.iterator]().next().value;
// 使用递归生成器迭代每一个node
for (const node of traverse([firstNode])) {
visitedNodes.add(node);
}
return visitedNodes.size === this.nodes.size;
}
}

使用生成器作为默认迭代器

因为生成器对象实现了可迭代对象接口,并且因为生成器函数和默认迭代器都被调用来生成迭代器,所以生成器非常适合用作默认迭代器。下面是一个简单的例子,默认迭代器可以在一行代码中中 yield 类的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Foo {
constructor() {
this.values = [1, 2, 3];
}
*[Symbol.iterator]() {
yield* this.values;
}
}
const f = new Foo();
for (const x of f) {
console.log(x);
}
// 1
// 2
// 3

在这里,for…of 循环调用默认迭代器(这恰好是一个生成器函数)并生成一个生成器对象。生成器对象是一个可迭代对象,因此可以在迭代中使用。

生成器提前终止

与迭代器一样,生成器也支持“可关闭(closable)”的概念。对于要实现迭代器接口的对象,具有必选的 next()和 可选的 return()方法,以便在迭代器提前终止时使用。生成器对象同时具有这两个方法及另外一个方法 throw()。

1
2
3
4
5
6
function* generatorFn() {}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.next); // f next() { [native code] }
console.log(g.return); // f return() { [native code] }
console.log(g.throw); // f throw() { [native code] }

return()和 throw()方法是可用于强制生成器进入关闭状态的两个方法。

return() 方法

return()方法将强制生成器进入关闭状态,return()的值将提供给最终的迭代器对象:

1
2
3
4
5
6
7
8
9
function* generatorFn() {
for (const x of [1, 2, 3]) {
yield x;
}
}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.return(4)); // { done: true, value: 4 }
console.log(g); // generatorFn {<closed>}

与迭代器不同,所有生成器对象都有一个 return()方法,该方法强制生成器进入一个关闭状态,一旦到达该状态就无法退出。后续调用 next()将显示 done:true 状态,但是任何提供的 return 值都不存储或传递:

1
2
3
4
5
6
7
8
9
function* generatorFn() {
for (const x of [1, 2, 3]) {
yield x;
const g = generatorFn();
console.log(g.next()); // { done: false, value: 1 }
console.log(g.return(4)); // { done: true, value: 4 }
console.log(g.next()); // { done: true, value: undefined }
console.log(g.next()); // { done: true, value: undefined }
console.log(g.next()); // { done: true, value: undefined }

内置的语言结构,例如 for…of 循环将明智地忽略 done:true 的迭代器对象中返回的任何值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* generatorFn() {
for (const x of [1, 2, 3]) {
yield x;
}
}
const g = generatorFn();
for (const x of g) {
if (x > 1) {
g.return(4);
}
console.log(x);
}
// 1
// 2
throw()方法

throw()方法将把提供的错误注入挂起的生成器对象中。如果错误未处理,生成器将关闭:

1
2
3
4
5
6
7
8
9
10
11
12
13
function* generatorFn() {
for (const x of [1, 2, 3]) {
yield x;
}
}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
try {
g.throw('foo');
} catch (e) {
console.log(e); // foo
}
console.log(g); // generatorFn {<closed>}

但是,如果错误是在生成器函数内部处理的,那么它将不会关闭并可以继续执行。错误处理过程将跳过这个结果,因此在下面将看到它跳过一个值。考虑下面的例子:

1
2
3
4
5
6
7
8
9
10
11
function* generatorFn() {
for (const x of [1, 2, 3]) {
try {
yield x;
} catch (e) {}
}
}
const g = generatorFn();
console.log(g.next()); // { done: false, value: 1}
g.throw('foo');
console.log(g.next()); // { done: false, value: 3}

在此示例中,生成器在 try / catch 块内的 yield 关键字处暂停执行。挂起时,throw()注入 foo 错误,该错误由 yield 关键字引发。由于此错误是在生成器的 try / catch 块中引发的,因此随后仍在生成器内部时被捕获。但是,由于 yield 抛出了这个错误,生成器将不会 yield 2 。相反,生成器函数继续执行,继续到下一个循环迭代,再次遇到 yield 关键字ーー这一次,yield 值为 3。

注意:如果生成器对象尚未开始执行,由于错误是从函数块外部抛出的,所以无法在函数内部捕获对 throw()的调用。

总结

迭代是基本上每种编程语言都会遇到的模式。ECMAScript6 规范通过在语言中引入迭代器和生成器这两个形式化概念,正式地确定了迭代。

迭代器是一个接口,可以由任何对象实现,并允许连续访问它生成的值。任何实现可迭代接口的对象都有默认的 Symbol.iterator 属性。默认迭代器的行为类似于迭代器工厂函数:当被调用时,生成一个实现具有迭代器接口的对象。

迭代器通过 next()方法强制生成值,该方法返回可迭代对象。该对象包含一个布尔值的 done 属性(指示是否还有更多可用值)以及一个 value 属性,其中包含从迭代器提供的当前值。该接口可以通过重复调用 next()来手动使用,也可以由原生的可迭代结构(例如 for … of 循环)自动使用。

生成器是一种特殊类型的函数,当调用它时,会生成一个生成器对象。这个生成器对象实现了可迭代对象接口,因此可以在任何需要迭代的地方使用。生成器的独特之处在于它们支持 yield 关键字,该关键字用于暂停生成器函数的执行。还可以使用 yield 关键字通过 next()方法接受输入和输出。当伴随星号时,yield 关键字将用于序列化与其配对的一个可迭代对象。

👆 全文结束,棒槌时间到 👇