Generator 是 ES6 提出的一种异步编程的解决办法,它与传统的函数完全不同,本章从基础概念和基本用法进行讲解和解析。在此之前也是对 Generator 函数云里雾里,所以通过此次学习,希望能对 Generator 有更深的理解和认识。

1.概念

Generator 中文的意思是’生成器’,阮一峰:ECMAScript 6 入门中对 Generator 解释是:
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
Generator 有两个特点:
第一个是在定义时,要在 function 关键字和函数名中加一个星号,第二个就是在函数体中运用 yeild 表达式表示不同的状态。

1
2
3
4
5
6
function* foo() {
yield "a";
yield "b";
}
let f = foo();
console.log(f);

和普通函数不同的是,当运行这个函数时,返回不是这个函数的结果,而是一个遍历器对象,或者可以说是一个含有内部状态指针的对象。
如果想输出值得话,要用 next 方法来进行输出。next 方法就是向下移动指针,即每次调用 next 方法,就是从函数头部或者从上一次 yield 表达式移动到下一次 yield 表达式(或者 return 为止)。

1
2
3
4
5
6
7
8
function* foo() {
yield "a";
yield "b";
}
let f = foo();
f.next(); // {value: "a", done: false}
f.next(); // {value: "b", done: false}
f.next(); // {value: undefined, done: true}

上述可以看出,调用 next 时输出的是一个对象,即 value 值代表 yield 后面的结果,done 代表遍历还没有结束,当遍历结束时,value 值都为 undefined,done 都为 true。

2.yield 表达式

yield 有个懒惰的特性,即 yield 后面的表达式,如果不调用 next 方法,是不会执行的。

1
2
3
4
function* foo() {
yield 1 + 1;
}
foo().next(); // 2

只有当 next 指针移动到该 yield 的时候,才会执行后面的表达式。
yield 和 return 是有不同之处的,在 Generator 函数中,可以定义多个 yield 表达式,但是 return 只能定义一个,并且在 yield 中遍历还没有完成,但在遇到 return 时,遍历就终止了。

1
2
3
4
5
6
7
8
9
10
function* foo() {
yield "a";
yield "b";
return "c";
}
let f = foo();
f.next(); // {value: "a", done: false}
f.next(); // {value: "b", done: false}
f.next(); // {value: "c", done: true}
f.next(); // {value: undefined, done: true}

当遇到 return 时,遍历结束 done 为 true,value 值为 return 后的结果,在此之后的 next 的结果都为{value: undefined, done: true}。
如果在 Generator 函数中没有 yield 表达式:

1
2
3
4
5
function* foo() {
console.log("a");
}
let f = foo();
f.next();

foo()返回的依旧是一个含有内部状态指针的对象,只有当 next 方法执行时该函数才会执行。
当 yield 和其他表达式融合时,如果 yield 表达式在左边,要将 yield 表达式用圆括号包裹,否则就会报语法错误。

1
2
3
4
5
6
7
8
function* foo() {
console.log('a'+ yield 'b'); // Uncaught SyntaxError: Unexpected identifier
}
function* foo() {
console.log('a'+ (yield 'b')); // {value: "b", done: false}
}
let f = foo();
f.next();

当用作函数参数或放在赋值表达式的右边,可以不加括号。

1
2
3
4
function* demo() {
foo(yield "a", yield "b"); // OK
let input = yield; // OK
}

3.next 方法

yield 是没有返回值的,它的返回值是 undefined,我们可以通过 next 方法将参数传递给 yield,此参数将会为 yield 的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
function* f() {
for (var i = 0; true; i++) {
var reset = yield i;
if (reset) {
i = -1;
}
}
}
var g = f();
g.next(); // { value: 0, done: false }
g.next(); // { value: 1, done: false }
g.next(true); // { value: 0, done: false }

上面代码先定义了一个可以无限运行的 Generator 函数 f,如果 next 方法没有参数,每次运行到 yield 表达式,变量 reset 的值总是 undefined。当 next 方法带一个参数 true 时,变量 reset 就被重置为这个参数(即 true),因此 i 会等于-1,下一轮循环就会从-1 开始递增。
不用 next 方法是否可以输出值呢?答案是可以的,可以用 for…of…方法遍历 Generator 函数。

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

for (let v of foo()) {
console.log(v); // 1 2 3 4 5
}

值得注意的是,for…of…在 done 为 ture 的时候就会终止执行,所以 return 后的 6 没有输出。

4.yield* 表达式

yield* 表达式是为了解决在一个 Generator 函数中调用另一个 Generator 函数所提供的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* foo() {
yield 1;
yield 2;
yield 3;
yield* foo1();
}
等同于;
function* foo() {
yield 1;
yield 2;
yield 3;
for (let v of foo1()) {
yield v;
}
}

当 yield*后边的 Generator 函数中没有 return 时,作用就是 for…of…遍历 Generator 函数,如果 Generator 函数中有 return 时,则获取的是 return 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* foo() {
yield 1;
yield 2;
yield 3;
var value = yield* foo1();
console.log(value);
}
function* foo1() {
yield 4;
return 5;
}
let f = foo();
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // 5 {value: undefined, done: true}

其实 yield*后面只要是带有 Iterator 接口的都会被遍历。

1
2
3
4
5
function* foo() {
yield* [1, 2, 3, 4];
}
let f = foo();
f.next(); // {value: 1, done: false}

再举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
function* foo() {
yield "a";
yield "b";
return "END";
}
function* bar(func) {
let result = yield* func();
console.log(result);
}
[...bar(foo)];
// END
// ["a", "b"]

上述中 foo 是拥有 return 表达式的函数,所以 return 后的结果会‘赋值’给 yield*表达式的返回值,所以 result 是’END’,并且拓展运算符默认调用 Iterator 接口,所以会先打印出 result,然后再执行 yield。

参考:
阮一峰:ECMAScript 6 入门