红宝书笔记(四):迭代器与生成器

第七章 迭代器与生成器

重点掌握:

  1. 迭代器模式;
  2. 生成器;

7.1 理解迭代

迭代:按照顺序【即有序集合上进行】反复多次执行一段程序,通常会有明确的终止条件。

ECMAScript6规范新增了两个高级特性:迭代器和生成器。

发者无须事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式。


7.2 迭代器模式

迭代器(iterator)是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的 API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。这种概念上的分离正是 Iterable 和 Iterator 的强大之处。

7.2.1 可迭代协议

实现 Iterable 接口(可迭代协议)要求同时具备两种能力:

  1. 支持迭代的自我识别能力;
  2. 创建实现Iterator 接口的对象的能力。

在 ECMAScript 中,这意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的 Symbol.iterator 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。

很多内置类型都实现了 Iterable 接口:

 字符串

 数组

 映射

 集合

 arguments 对象

 Node List 等 DOM 集合类型

实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性。接收可迭代对象的原生语言特性包括:

 for-of 循环

 数组解构

 扩展操作符

 Array.from()

 创建集合

 创建映射

 Promise.all()接收由期约组成的可迭代对象

 Promise.race()接收由期约组成的可迭代对象

 yield*操作符,在生成器中使用

7.2.2 迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器 API 使用 next()方法在可迭代对象中遍历数据。每次成功调用 next(),都会返回一个 Iterator Result 对象,其中包含迭代器返回的下一个值。若不调用 next(),则无法知道迭代器的当前位置。

next()方法返回的迭代器对象 Iterator Result 包含两个属性:done 和 value。done 是一个布尔值,表示是否还可以再次调用 next()取得下一个值;value 包含可迭代对象的下一个值(done 为false),或者 undefined(done 为 true)。done: true 状态称为“耗尽”。可以通过以下简单的数组来演示:

1
2
3
4
5
6
7
8
9
10
// 可迭代对象 
let arr = ['foo', 'bar'];
// 迭代器工厂函数
console.log(arr[Symbol.iterator]); // f values() { [native code] }
// 迭代器
let iter = arr[Symbol.iterator](); console.log(iter); // Array Iterator {}
// 执行迭代
console.log(iter.next()); // { done: false, value: 'foo' }
console.log(iter.next()); // { done: false, value: 'bar' }
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' }

迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程。 8 如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化:

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 }

注意: 迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可 迭代对象。

7.2.3 自定义迭代器

与 Iterable 接口类似,任何实现 Iterator 接口的对象都可以作为迭代器使用。下面这个例子中 的 Counter 类只能被迭代一定的次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

这个类实现了 Iterator 接口,但不理想。这是因为它的每个实例只能被迭代一次:

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

为了让一个可迭代对象能够创建多个迭代器,必须每创建一个迭代器就对应一个新计数器。为此,可以把计数器变量放到闭包里,然后通过闭包返回迭代器:

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
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

每个以这种方式创建的迭代器也实现了 Iterable 接口。Symbol.iterator 属性引用的工厂函数 会返回相同的迭代器:

1
2
3
4
5
6
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

7.2.4 提前终止迭代器

可选的 return()方法用于指定在迭代器提前关闭时执行的逻辑。

return()方法必须返回一个有效的 IteratorResult 对象。简单情况下,可以只返回{ done: true }。 因为这个返回值只会用在生成器的上下文中。


7.3 生成器

生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要是可以定义 函数的地方,就可以定义生成器。

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() {}
}

注意:箭头函数不能用来定义生成器函数。

7.3.1 生成器基础

标识生成器函数的星号不受两侧空格的影响。

调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行( suspended)的状态。与迭代器相似,生成器对象也实现了 Iterator 接口,因此具有 next()方法。调用这个方法会让生成器开始或恢复执行。

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

7.3.2 通过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
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' }

yield 关键字只能在生成器函数内部使用,用在其他地方会抛出错误。类似函数的 return 关键字,yield 关键字必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误:

1
2
3
4
5
6
7
8
9
10
// 有效
function* validGeneratorFn() {
yield;
}
// 无效
function* invalidGeneratorFnA() {
function a() {
yield;
}
}
  1. 生成器对象作为可迭代对象
  2. 使用 yield 实现输入和输出

除了可以作为函数的中间返回语句使用, yield 关键字还可以作为函数的中间参数使用。上一次让生成器函数暂停的 yield 关键字会接收到传给 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”。下一次调用 next()传入了”bar”,作为交给同一个 yield 的值。然后这个值被确定为本次生成器函数要返回的值。

7.3.3 生成器作为默认迭代器

7.3.4 提前终止迭代器


7.4 小结