ES6 中的生成器

生成器 generator

语法

1
2
3
function *foo() {
// ..
}

* 的位置可以改变,写成下面的几种形式都可以:

1
2
3
4
function *foo()  { .. }
function* foo() { .. }
function * foo() { .. }
function*foo() { .. }

在对象字面量中,函数成员有简化形式,生成器同样也有:

1
2
3
var a = {
*foo() { .. }
};

调用生成器

调用生成器和调用普通的函数一样,函数名加上 ()

1
2
3
4
5
6
7
8
foo();

// 有参数的形式
function *foo(x,y) {
// ..
}

foo( 5, 10 );

和普通函数的主要差别是调用生成器时,并不是直接运行生成器中的代码。而是会生成一个迭代器来控制生成器代码的运行。

yield

生成器使用了一个新的关键字 yield

1
2
3
4
5
6
7
8
function *foo() {
var x = 10;
var y = 20;

yield;

var z = x + y;
}

普通的函数正常情况下从调用开始运行,一直到 return .. 语句结束运行。 而生成器则不是这样,它可以在代码运行的过程中停止, yield 关键字出现的位置就是停止点。

在上面的例子中,前两行代码会先运行,然后遇到 yield ,后续的代码会暂停运行。使用迭代器 next() 恢复运行之后,最后一行代码才会执行。 注意,yield 能够在一个生成器中出现多次,也可以出现在循环当中:

1
2
3
4
5
function *foo() {
while (true) {
yield Math.random();
}
}

yield 不仅仅可以将值从函数中输出来,也可以通过迭代器的 next(..) 输入进函数:

1
2
3
4
function *foo() {
var x = yield 10;
console.log( x );
}

生成器首先在第一次停止的时候输出一个10,然后使用 it.next(..) 恢复生成器的时候,从 next(..) 传进来的参数会取代 yield 10 赋值给 x

yield .. 类似于一个赋值表达式 a=.., 赋值表达式可以出现的地方, yield 都可以出现:

1
2
3
4
5
6
7
8
9
var a, b;

a = 3; // valid
b = 2 + a = 3; // invalid
b = 2 + (a = 3); // valid

yield 3; // valid
a = 2 + yield 3; // invalid
a = 2 + (yield 3); // valid

yield *

yield * 叫做yield 委托,在语法上 yield * ..yield 表现的一样:

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

yield * .. 后面需要跟一个可迭代的值,然后调用这个迭代器,然后让迭代器接管这个生成器,直到迭代完毕:

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

数组 [1,2,3] 会生成一个迭代器遍历这些值,而 *foo() 生成器会一步一步输出这些值。

另外 yield *.. 后面还可以跟一个生成器:

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

function *bar() {
yield *foo();
}

*foo() 输出的值,在 *bar() 中也会输出。看一个例子:

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

function *bar() {
var x = yield *foo();
console.log( "x:", x );
}

for (var v of bar()) {
console.log( v );
}
// 1 2 3
// x: 4

1,2,3 首先从 *foo 中输出,然后再从 *bar() 中输出,4*foo() 的返回值,会用来给 x 赋值。

生成器的控制与迭代器一样,通过 next(), return(), throw() 控制他继续运行,或者提前结束又或是抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function *foo() {
yield 1;
yield 2;
yield 3;
}

var it1 = foo();
it1.next(); // { value: 1, done: false }
it1.next(); // { value: 2, done: false }

var it2 = foo();
it2.next(); // { value: 1, done: false }

it1.next(); // { value: 3, done: false }

it2.next(); // { value: 2, done: false }
it2.next(); // { value: 3, done: false }

it2.next(); // { value: undefined, done: true }
it1.next(); // { value: undefined, done: true }

上面这个例子说明了,可以通过多个迭代器同时控制一个生成器,并且互相之间相互不干扰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function *foo() {
try {
yield 1;
yield 2;
yield 3;
}
finally {
console.log( "cleanup!" );
}
}

for (var v of foo()) {
console.log( v );
}
// 1 2 3
// cleanup!

var it = foo();

it.next(); // { value: 1, done: false }
it.return( 42 ); // cleanup!
// { value: 42, done: true }

上面这个例子说明了foo..of.. 可以进行迭代器迭代工作,利用 return 可以提前结束生成器的执行,并且完成 finally 语句块中的清理工作。

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 *foo() {
try {
yield 1;
}
catch (err) {
console.log( err );
}

yield 2;

throw "Hello!";
}

var it = foo();

it.next(); // { value: 1, done: false }

try {
it.throw( "Hi!" ); // Hi!
// { value: 2, done: false }
it.next();

console.log( "never gets here" );
}
catch (err) {
console.log( err ); // Hello!
}

上面是一个错误处理的例子,首先调用生成器,返回一个迭代器赋值给 it, 调用 it.next() ,输出 *foo 中的 try 块中的 yield 1 的结果。

然后主流程中的 try 语句中调用 it.throw() 抛出一个异常,生成器捕捉到这个基础进行输出。然后生成器继续执行到 yield 2 语句中止,输出 { value: 2, done: false } 。然后主流程中的 try 块继续运行 it.next(), 生成器抛出一个异常, it.next() 没有得到{done:true}的结果,这个异常被主流程捕捉到进行输出。

剖析生成器

为了更好的理解生成器的原理,我们进行人工转换,将生成器转换成ES5的代码。

首先,我们的生成器非常简单:

1
2
3
4
function *foo() {
var x = yield 42;
console.log( x );
}

这是一个函数,调用这个函数返回一个迭代器:

1
2
3
4
5
6
7
function foo(){
return {
next: function() {..}

// we'll skip `return(..)` and `throw(..)`
}
}

接下来分析这个 next()next() 返回值是 {value: 42, done: false}{value: undifined, done: true}。所以有:

1
2
3
4
5
6
7
8
9
10
function foo(){
return {
next: function() {
// get value and state
return {value: value, done: state};
}

// we'll skip `return(..)` and `throw(..)`
}
}

生成器到完成状态,要调用两次next,那么我们设置两个状态:

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
function foo(){
var state = 0
return {
next: function() {
switch (state) {
case 0:
var value = 42,
done = false;
break;
case 1:
value = undefined,
done = true;
break;
}
state ++ ;
return {value: value, done: done};
}

// we'll skip `return(..)` and `throw(..)`
}
}

var it = foo();
console.log(it.next()) // {"value":42,"done":false}
console.log(it.next()) // {"done":true}

利用一个闭包来控制调用 next 的状态。但是这与我们的生成器还是有点不同,在第二次调用 it.next() 时,可以传入一个参数给 x 赋值,并且打印出来,所以我们可以改写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function foo(){
var state = 0
return {
next: function(xx) {
switch (state) {
case 0:
var value = 42,
done = false;
break;
case 1:
value = undefined,
done = true;
var x = xx;
console.log(x);
break;
}
state ++ ;
return {value: value, done: done};
}

// we'll skip `return(..)` and `throw(..)`
}
}

那么一个生成器就这样该写出来了。

总结

ES6 引入了生成器,生成器提供了很强大的功能:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。