ECMAScript6 的 Generator 学习小结

Generator 是 ECMAScript6 提供的新特性之一,最大的特点是可以在其内部暂停执行。它可以用来生成一个迭代器。通过迭代器器不断往下执行,我们可以让代码在 Generator 内部暂停或继续执行。

什么是 Generator

Generator 函数的形式类似于一个普通函数,但其语法看上去与普通函数有以下区别。

  • function 关键字与函数名之间有一个星号;
  • 函数体内部可以使用 yieldyield* 关键字定义迭代器的每个成员。

Generator 的行为也与普通函数不一样:

  • 执行 Generator 函数会返回一个迭代器对象,而不是执行它的函数体内容。
  • 它所生成的迭代器对象可以分段地执行 Generator 的函数体。

看个例子:

1
2
3
4
5
6
7
8
9
function* numberGenerator() {
yield 1;
yield 2;
return 3;
}
var numbers = numberGenerator();
numbers.next(); // 输出:{ value: 1, done: false }
numbers.next(); // 输出:{ value: 2, done: false }
numbers.next(); // 输出:{ value: 3, done: true }

  1. 这段代码定义了一个 Generator,然后像一个函数一样调用它。
  2. 注意调用它时不会立即执行其内部代码,只会得到一个迭代器(numbers)。
  3. 在此之后每次调用迭代器的 next() 方法,就从函数体的开头(第一次调用 next() 方法时)或上一次停下来的地方(第二次或之后调用 next() 方法时)继续执行,直到遇到下一个 yield 语句停止。
  4. 每一次执行 next() 方法都返回一个对象,对象的 value 属性就是当前 yield 语句后面的表达式的值,done 属性表示迭代器是否已经遍历结束。
  5. 不用 yield 语句的 Generator,就是一个普通的暂缓执行函数,因为调用一次 next() 就相当于执行函数。

遍历迭代器

既然 Generator 函数调用时返回的结果是一个迭代器对象,那么除了可以调用连续它的 next() 方法来遍历它之外,我们也可以用 for of 来遍历这个迭代器对象。例如:

1
2
3
4
5
6
7
8
function* getNumbers() {
yield 1;
yield 2;
yield 3;
}
for (var i of getNumbers()) {
console.log(i);
}

这段代码会依次输出 1,2,3。

yield 和 yield*

1
2
3
4
5
6
7
8
9
10
11
12
13
function* getDelegate() {
    yield 2;
    yield 3;
    yield 4;
}
function* getNumbers() {
    yield 1;
    yield* getDelegate();
    yield 5;
}
for(var i of getNumbers()) {
    console.log(i);
}

这段代码会依次输出 1,2,3,4,5。
yield 后面可以跟任何表达式,表达式的值将作为迭代器调用 next() 时的返回对象中的 value 属性值。
yield* 后面只能跟一个另一个迭代器,效果相当于遍历该迭代器。可以理解为,当执行 next() 函数遇到了 yield* 语句时,执行权限被交给了 yield* 后面那个迭代器,当这个迭代器执行完(即done==false)时,执行权限又被交回来。

由于数组本来就支持迭代器遍历,所以 yield* 后面可以跟一个数据。例如:

1
2
3
4
5
6
7
8
9
10
function* getNumbers() {
    yield 1;
    yield* [2, 3, 4];
    yield 5;
}
for(var i of getNumbers()) {
    console.log(i);
}
// 依次输出 1,2,3,4,5

yield 语句的值和 next() 方法

前面提到,调用迭代器的 next() 方法可以从函数头部或者上一个 yield 语句开始执行,一直执行到下一个 yield 语句。如果在调用 next() 方法时传入一个参数,则该参数会作为上一个 yield 语句的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function* getNumbers() {
    var a = yield 1;
console.log('a', a);
var b = yield 2;
console.log('b', b);
return a + b;
}
var g = getNumbers();
console.log(g.next());
// 输出:
// { value: 1, done: false }
// a 200
console.log(g.next(200));
// 输出:
// { value: 2, done: false }
// b 300
console.log(g.next(300));
// 输出:
// { value: 500, done: true }

  • 第一次,调用 next() 时,第一个 yield 语句后面的表达式值是1,所以输出 {value: 1, done: false}。
  • 第二次,调用 next(200) 时,传入 200,所以上一个 yield 语句的返回值被设置为 200,赋值给了 a。next(200) 的返回值是第二个 yield 语句后面的表达式的值,也就是2,所以输出{value: 2, done: false}。
  • 第三次,调用 next(300) 时,传入 300,所以上一个 yield 语句的返回值被设置为 300,赋值给了 b。此时代码走到了 return 语句,返回了 a+b 的值为500,而且迭代器编创结束,所以输出{value: 500, done: true}。

对 Generator 的使用

  1. Generator 天生提供了一种保存当前代码执行状态的能力,例如:
    1
    2
    3
    4
    5
    6
    7
    8
    var clock = function*() {
    while (true) {
    console.log('Tick');
    yield;
    console.log('Tock');
    yield;
    }
    };

这个 clock 函数有两种状态:Tick 和 Tock,每执行一次迭代器的 next() 方法,状态就改变一次。同时用一个死循环来保证迭代器的 done 属性值永远是 false。如果安装传统 JS 的写法,实现这样一个方法就需要有一个变量来保存当前的状态。相比之下,使用 Generator 无非更简洁。

  1. 由于调用迭代器的 next() 方法时可以传参数改变函数内的值,所以为写出灵活的代码提供了更多的可能性。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function* selfIncreaseGenerator() {
    var value = 0;
    var reset = false;
    while (true) {
    if (reset) {
    value = 0;
    reset = false;
    }
    reset = yield value++;
    }
    }
    var selfIncrease = selfIncreaseGenerator();
    console.log(selfIncrease.next().value); // 输出:0
    console.log(selfIncrease.next().value); // 输出:1
    console.log(selfIncrease.next().value); // 输出:2
    console.log(selfIncrease.next(true).value); // 输出:0
    console.log(selfIncrease.next().value); // 输出:1
    console.log(selfIncrease.next().value); // 输出:2

这里实现了一个计数器,每一次调用 next() 返回的值都比上一次大1。通过 next(true) 可以把计数器重置回0。