Post

JavaScript红宝书再阅读(2)

第7章 迭代器与生成器

1. 迭代器

迭代器模式描述了一个方案,即可以把有些实现了正式的Iterable接口,而且可以通过迭代器Iterator消费的结构称为”可迭代对象“(iterable)。

迭代器无需了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。

可迭代协议(实现Iterable接口):

需要具备的两种能力:

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

在ES中,上述两点意味着:

  1. 必须暴露一个属性作为”默认迭代器“,而且这个属性必须使用Symbol.iterator作为键。
  2. 这个迭代器必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。

实现了该接口的内置类型:

字符串、数组、映射(Map)、集合(Set)、arguments对象、NodeList等DOM集合类型

接收可迭代对象的原生语言特性(自然调用迭代器对象Symbol.interator):

for-of循环、数组解构、扩展操作符、Array.from()、创建集合、创建映射、Promise.all()接收有Promise组成的可迭代对象、Promise.race()接收有Promise组成的可迭代对象、yield*操作符

Ex.:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let num = 1;
let obj =  {};
let str = 'abc';

console.log(num[Symbol.iterator]);   // undefined
console.log(obj[Symbol.iterator]);   // undefined
console.log(str[Symbol.iterator]);   // f values() {} 
//调用这个生成器会生成一个迭代器
console.log(str[Symbol.iterator]()); // StringIterator(); 

//如果对象原型链上的父类实现了Iterable接口,那这个对象也实现了这个接口
class FooArray extends Array{}
let fooArray = new FooArray('foo','bar','baz');

for(let el of fooArr) {
	console.log(el);
}

迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API使用next()方法在可迭代对象中遍历数据。

每次成功调用next(),都会返回一个IteratorResult对象,包含两个值:

  1. value:包含可迭代对象的下一个值(done为false)、undefined(done为true)
  2. done:一个布尔值,表示是否还可以再次调用next()取得下一个值。true为耗尽。

迭代器的两点注意点:

  1. 每个迭代器得到的实例之间没有关系,只会独立地遍历可迭代对象。
  2. 迭代器并不与迭代对象的某个时刻的快照绑定,如果对象被修改了那么迭代器也会反映变化。

(关于2的个人想法,所以对于数组的forEach修改,前一个如果修改,那么后一个得到的内容将会是修改后的)(虽然和这个无关,但是亲测是正确)

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

Ex.

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
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得到的值都为一样。
console.log(iter.next());    // {done: true, value: undefined}

// 每个迭代器得到的实例之间没有关系,只会独立地遍历可迭代对象
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'}

//迭代器并不与迭代对象的某个时刻的快照绑定,如果对象被修改了那么迭代器也会反映变化。
let iter3 = arr[Symbol.iterator]();
console.log(iter3.next()); // {done: false, value: 'foo'}
arr.splice(1, 0, 'baz');

console.log(iter3.next()); // {done: false, value: 'baz'}
console.log(iter3.next()); // {done: false, value: 'bar'}

自定义迭代器

任何实现了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
25
26
27
class Counter {
	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
for(let i of counter) console.log(i);
// (nothing)

上述对象因为直接修改了实例对象中的内容,所以每个实例的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
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 {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

提前终止迭代器

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

可能的情况:

  1. for-of循环通过break、continue、return或throw提前退出
  2. 结构操作并未消费所有值。

return()方法必须返回一个有效的IteratorResult对象。简单情况下,可以只返回{done: true}

如果迭代器没有被关闭(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
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 {done: true, value: undefined};	
			},
			return() {
				console.log('Existing early');
				return {done: true};
			}
		}
	}
}

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

//如果迭代器没有被关闭(done没有被设置为true),那么还可以继续从上次断开的地方继续迭代。
//数组的迭代器是不能被关闭的。

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
}

2. 生成器

何为生成器

生成器是ES6新增的一个极为灵活的结构,拥有在一个函数块中暂停和回复代码执行的能力。这个能力可以自定义迭代器和实现协程。

生成器基础

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

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

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

next返回的value为生成器函数的返回值,默认值为undefined。可以通过return 来指定。

生成器函数只会在初次调用next()方法后开始执行。

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

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
//生成器函数声明
function* generatorFn() {}
//生成器函数表达式
let generatorFn = function* () {}
//作为对象字面量方法的生成器函数
let foo = {
	* generatorFn () {},
}
//作为类实例方法的生成器函数
class Foo {
	* generatorFn() {};
}
//作为类静态方法的生成器函数
class Bar {
	static * generatorFn() {};
}

//生成器对象的生成
const g = generatorFn();
console.log(g) // generatorFn( <suspended> )
console.log(g.next) // f next() { [native code] }
console.log(g.next()) // { done: true, value: undefined } 

function* generatorFn1() {
	console.log('foo');
}
//初次调用并不会打印日志
let gO = generatorFn1();
//手动调用next之后才会调用。
generatorFn1.next(); // foo

//自引用迭代器
console.log(g === g[Symbol.iterator]()) // true

使用yield来中断执行

生成器函数在遇到yield之前会正常执行。遇到之后,会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过生成器对象上调用next()来恢复执行。

yield有点类似于函数的中间返回语句。通过yield退出的生成器函数会处在{ done: false }的状态。通过return返回的done会成为true。

生成器内部的执行流程会针对每一个生成器对象区分作用域。在一个生成器对象上调用next()不会影响其他生成器(类似于迭代器的机制)。

yield只能直接在生成器函数内部使用,在其他地方或者嵌套使用会抛出错误。

yield的作用:

  1. 生成器对象作为可迭代对象。
  2. 使用yield实现输入和输出。
  3. 产生可迭代对象(yield *)。

    yield* 实际上只是讲一个可迭代对象序列化为一连串可以单独阐述的值。所以这把yield放入一个循环中并没有什么不同。yield*的值是关联迭代器返回done: true时的value属性,对于普通迭代器来说,这个值是undefined,对于生成函数产生的迭代器来说,这个值是return的值。

  4. 使用yield*实现递归算法。

    这个内容可以用来实现图,并且实现深度优先遍历等图的算法(具体代码参考P200-201)

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// 1 生成器对象作为可迭代对象。
function* generatorFn() {
	yield 1;
	yield 2;
	yield 3;
}

for(const x of generatorFn()) {
	console.log(x);
}
//1 2 3

// 需要自定义迭代对象时,这样使用生成器对象会比较有用:
// 需要定义一个可迭代对象,他会产生一个迭代器,这个迭代器会执行指定的次数。
function* nTimes(n) {
	while(n--) yield;
}
for (let _ of nTimes(3)) {
	console.log('foo');
}
// foo foo foo

// 2 使用yield实现输入和输出。
function* generator2(init){
	console.log(init);
	console.log(yield);
	console.log(yield);
}

// yield位置会接受到next方法的第一个参数。
// 但是第一次调用next传入的直不会被使用,因为这只是开始生成器
let g = generator2('foo');

g.next('bar') // foo
g.next('baz') // baz  (attention!)

//yield也可以输出内容
function* ge3(){
	return yield 'foo';
}
let g2 = ge3();
//先遇到yield传出foo,然后在return yield传入的内容bar
console.log(g2.next());  // { done: false, value: 'foo' }
console.log(g2.next('bar')); // { done: true, value: 'bar' }
//无穷计数器
function* infi() {
	for(let i = 0; ; ++i){
		yield i;
	}
}

let infig = infi();
console.log(infig.next().value) // 1
console.log(infig.next().value) // 2
//...

//亦可以实现范围和填充数组
function* range(start, end) {
	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))) // 8个0的数组

// 3 yield* 

function* ge4 () {
	for (const x of [1,2,3] yield x;
}

function* ge4equal() {
	yield* [1,2,3];
}

// 4 递归算法
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 

生成器作为默认迭代器

这里for-of循环调用了默认迭代器(是一个生成器函数)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo {
	constructor(){
		this.value = [1,2,3];
	}
	*[Symbol.iterator](){
		yield* this.value;	
	}
}

const foo = new Foo();
for(const x of foo) console.log(x);

// 1 2 3

提前终止迭代器

return和throw方法都可以用于生成器进入关闭状态。

与迭代器不同,所有生成器都有return(),一旦调用就无法回复了。

for-of循环等语言内置语言结构会忽略状态done为true的返回值。

throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中,如果没有被处理生成器就会被关闭。不过假如生成器函数内部处理了这个错误,那么就不会被关闭,并且可以恢复执行,错误处理会跳过相应的yield。

注意:如果生成器对象还没有执行,那么调用的throw抛出的错误不会在函数内部被捕获,相当于在函数块外部抛出了错误。

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
function* geThrow() {
	yield* [1,2,3];
}

const g = geThrow();

try {
	g,throw('foo');
} catch(e) {
	console.log(e); // foo
}
console.log(g) // closed

//内部处理了error
function* geCatch() {
	for( const x of [1,2,3]) {
		try {
			yield x
		} catch(e){};
	}
}

const gC = geCatch();
console.log(gC.next()); // {done: false, value: 1} 
// 因为yield抛出的错误在内部被捕获,所以不会产出2
gC.throw('foo');
console.log(gC.next()); // {done: false, value: 3}
This post is licensed under CC BY 4.0 by the author.