ES6 Generator 函数



  • 定义和使用

    Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍 Generator 函数的语法和 API,它的异步编程应用请看《Generator 函数的异步应用》一章。
    Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
    执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
    形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式,定义不同的内部状态(yield 在英语里的意思就是“产出”)。
    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    
    var hw = helloWorldGenerator();
    
    
    上面代码定义了一个 Generator 函数 helloWorldGenerator,它内部有两个 yield 表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
    ES6 没有规定,function 关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
    function * foo(x, y) { ··· }
    function *foo(x, y) { ··· }
    function* foo(x, y) { ··· }
    function*foo(x, y) { ··· }
    
    
    由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在function 关键字后面。本书也采用这种写法。
  • next 方法的参数

    yield 表达式本身没有返回值,或者说总是返回 undefinednext 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。
    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 开始递增。
    这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过 next 方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
    再看一个例子。
    function* foo(x) {
      var y = 2 * (yield (x + 1));
      var z = yield (y / 3);
      return (x + y + z);
    }
    
    var a = foo(5);
    a.next() // Object{value:6, done:false}
    a.next() // Object{value:NaN, done:false}
    a.next() // Object{value:NaN, done:true}
    
    var b = foo(5);
    b.next() // { value:6, done:false }
    b.next(12) // { value:8, done:false }
    b.next(13) // { value:42, done:true }
    
    
    上面代码中,第二次运行 next 方法的时候不带参数,导致 y 的值等于 2 * undefined(即NaN),除以 3 以后还是 NaN,因此返回对象的 value 属性也等于 NaN。第三次运行 Next方法的时候不带参数,所以 z 等于 undefined,返回对象的 value 属性等于5 + NaN + undefined,即 NaN
    再看一个通过 next 方法的参数,向 Generator 函数内部输入值的例子。
    function* dataConsumer() {
      console.log('Started');
      console.log(`1. ${yield}`);
      console.log(`2. ${yield}`);
      return 'result';
    }
    
    let genObj = dataConsumer();
    genObj.next();
    // Started
    genObj.next('a')
    // 1. a
    genObj.next('b')
    // 2. b
    
    
    上面代码是一个很直观的例子,每次通过 next 方法向 Generator 函数输入值,然后打印出来。
    如果想要第一次调用 next 方法时,就能够输入值,可以在 Generator 函数外面再包一层。
    function wrapper(generatorFunction) {
      return function (...args) {
        let generatorObject = generatorFunction(...args);
        generatorObject.next();
        return generatorObject;
      };
    }
    
    const wrapped = wrapper(function* () {
      console.log(`First input: ${yield}`);
      return 'DONE';
    });
    
    wrapped().next('hello!')
    // First input: hello!
    
    
    上面代码中,Generator 函数如果不用 wrapper 先包一层,是无法第一次调用 next 方法,就输入参数的。
  • for...of 循环

    for...of 循环可以自动遍历 Generator 函数运行时生成的 Iterator 对象,且此时不再需要调用 next 方法。
    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 循环,依次显示 5 个 yield 表达式的值。这里需要注意,一旦 next 方法的返回对象的 done 属性为 true,for...of 循环就会中止,且不包含该返回对象,所以上面代码的 return 语句返回的 6,不包括在 for...of 循环之中。
    下面是一个利用 Generator 函数和 for...of 循环,实现斐波那契数列的例子。
    function* fibonacci() {
      let [prev, curr] = [0, 1];
      for (;;) {
        yield curr;
        [prev, curr] = [curr, prev + curr];
      }
    }
    
    for (let n of fibonacci()) {
      if (n > 1000) break;
      console.log(n);
    }
    
    
    从上面代码可见,使用 for...of 语句时不需要使用 next 方法。
    利用 for...of 循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用 for...of 循环,通过 Generator 函数为它加上这个接口,就可以用了。
    function* objectEntries(obj) {
      let propKeys = Reflect.ownKeys(obj);
    
      for (let propKey of propKeys) {
        yield [propKey, obj[propKey]];
      }
    }
    
    let jane = { first: 'Jane', last: 'Doe' };
    
    for (let [key, value] of objectEntries(jane)) {
      console.log(`${key}: ${value}`);
    }
    // first: Jane
    // last: Doe
    
    
    上面代码中,对象 jane 原生不具备 Iterator 接口,无法用 for...of 遍历。这时,我们通过 Generator 函数 objectEntries 为它加上遍历器接口,就可以用 for...of 遍历了。加上遍历器接口的另一种写法是,将 Generator 函数加到对象的 Symbol.iterator 属性上面。
    function* objectEntries() {
      let propKeys = Object.keys(this);
    
      for (let propKey of propKeys) {
        yield [propKey, this[propKey]];
      }
    }
    
    let jane = { first: 'Jane', last: 'Doe' };
    
    jane[Symbol.iterator] = objectEntries;
    
    for (let [key, value] of jane) {
      console.log(`${key}: ${value}`);
    }
    // first: Jane
    // last: Doe
    
    
    除了 for...of 循环以外,扩展运算符(...)、解构赋值和 Array.from 方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。
    function* numbers () {
      yield 1
      yield 2
      return 3
      yield 4
    }
    
    // 扩展运算符
    [...numbers()] // [1, 2]
    
    // Array.from 方法
    Array.from(numbers()) // [1, 2]
    
    // 解构赋值
    let [x, y] = numbers();
    x // 1
    y // 2
    
    // for...of 循环
    for (let n of numbers()) {
      console.log(n)
    }
    // 1
    // 2
    
    
    更多的 for...of 循环,请参考《for...of循环》这章
  • Generator.prototype.throw()

    Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
    var g = function* () {
      try {
        yield;
      } catch (e) {
        console.log('内部捕获', e);
      }
    };
    
    var i = g();
    i.next();
    
    try {
      i.throw('a');
      i.throw('b');
    } catch (e) {
      console.log('外部捕获', e);
    }
    // 内部捕获 a
    // 外部捕获 b
    
    
    上面代码中,遍历器对象 i 连续抛出两个错误。第一个错误被 Generator 函数体内的 catch 语句捕获。i 第二次抛出错误,由于 Generator 函数内部的 catch 语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的 catch 语句捕获。
    throw 方法可以接受一个参数,该参数会被 catch 语句接收,建议抛出 Error 对象的实例。
    var g = function* () {
      try {
        yield;
      } catch (e) {
        console.log(e);
      }
    };
    
    var i = g();
    i.next();
    i.throw(new Error('出错了!'));
    // Error: 出错了!(…)
    
    
    注意,不要混淆遍历器对象的 throw 方法和全局的 throw 命令。上面代码的错误,是用遍历器对象的 throw 方法抛出的,而不是用 throw 命令抛出的。后者只能被函数体外的 catch 语句捕获。
    var g = function* () {
      while (true) {
        try {
          yield;
        } catch (e) {
          if (e != 'a') throw e;
          console.log('内部捕获', e);
        }
      }
    };
    
    var i = g();
    i.next();
    
    try {
      throw new Error('a');
      throw new Error('b');
    } catch (e) {
      console.log('外部捕获', e);
    }
    // 外部捕获 [Error: a]
    
    
    上面代码之所以只捕获了 a,是因为函数体外的 catch 语句块,捕获了抛出的 a 错误以后,就不会再继续 try 代码块里面剩余的语句了。
    如果 Generator 函数内部没有部署 try...catch 代码块,那么 throw 方法抛出的错误,将被外部 try...catch 代码块捕获。
    var g = function* () {
      while (true) {
        yield;
        console.log('内部捕获', e);
      }
    };
    
    var i = g();
    i.next();
    
    try {
      i.throw('a');
      i.throw('b');
    } catch (e) {
      console.log('外部捕获', e);
    }
    // 外部捕获 a
    
    
    上面代码中,Generator 函数 g 内部没有部署 try...catch 代码块,所以抛出的错误直接被外部 catch 代码块捕获。
    如果 Generator 函数内部和外部,都没有部署 try...catch 代码块,那么程序将报错,直接中断执行。
    var gen = function* gen(){
      yield console.log('hello');
      yield console.log('world');
    }
    
    var g = gen();
    g.next();
    g.throw();
    // hello
    // Uncaught undefined
    
    
    上面代码中,g.throw 抛出错误以后,没有任何 try...catch 代码块可以捕获这个错误,导致程序报错,中断执行。
    throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next 方法。
    function* gen() {
      try {
        yield 1;
      } catch (e) {
        console.log('内部捕获');
      }
    }
    
    var g = gen();
    g.throw(1);
    // Uncaught 1
    
    
    上面代码中,g.throw(1) 执行时,next 方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。这种行为其实很好理解,因为第一次执行 next 方法,等同于启动执行 Generator 函数的内部代码,否则 Generator 函数还没有开始执行,这时 throw 方法抛错只可能抛出在函数外部。
    throw 方法被捕获以后,会附带执行下一条 yield 表达式。也就是说,会附带执行一次 next 方法。
    var gen = function* gen(){
      try {
        yield console.log('a');
      } catch (e) {
        // ...
      }
      yield console.log('b');
      yield console.log('c');
    }
    
    var g = gen();
    g.next() // a
    g.throw() // b
    g.next() // c
    
    
    上面代码中,g.throw 方法被捕获以后,自动执行了一次 next 方法,所以会打印 b。另外,也可以看到,只要 Generator 函数内部部署了 try...catch 代码块,那么遍历器的 throw 方法抛出的错误,不影响下一次遍历。
    一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用 next 方法,将返回一个 value 属性等于 undefineddone 属性等于 true 的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。
    function* g() {
      yield 1;
      console.log('throwing an exception');
      throw new Error('generator broke!');
      yield 2;
      yield 3;
    }
    
    function log(generator) {
      var v;
      console.log('starting generator');
      try {
        v = generator.next();
        console.log('第一次运行next方法', v);
      } catch (err) {
        console.log('捕捉错误', v);
      }
      try {
        v = generator.next();
        console.log('第二次运行next方法', v);
      } catch (err) {
        console.log('捕捉错误', v);
      }
      try {
        v = generator.next();
        console.log('第三次运行next方法', v);
      } catch (err) {
        console.log('捕捉错误', v);
      }
      console.log('caller done');
    }
    
    log(g());
    // starting generator
    // 第一次运行next方法 { value: 1, done: false }
    // throwing an exception
    // 捕捉错误 { value: 1, done: false }
    // 第三次运行next方法 { value: undefined, done: true }
    // caller done
    
    
    上面代码一共三次运行 next 方法,第二次运行的时候会抛出错误,然后第三次运行的时候,Generator 函数就已经结束了,不再执行下去了。
  • Generator.prototype.return()

    Generator 函数返回的遍历器对象,还有一个 return 方法,可以返回给定的值,并且终结遍历 Generator 函数。
    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    var g = gen();
    
    g.next()        // { value: 1, done: false }
    g.return('foo') // { value: "foo", done: true }
    g.next()        // { value: undefined, done: true }
    
    
    上面代码中,遍历器对象 g 调用 return 方法后,返回值的 value 属性就是 return 方法的参数 foo。并且,Generator 函数的遍历就终止了,返回值的 done 属性为 true,以后再调用 next 方法,done 属性总是返回 true
    如果 return 方法调用时,不提供参数,则返回值的 value 属性为 undefined
    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    var g = gen();
    
    g.next()        // { value: 1, done: false }
    g.return() // { value: undefined, done: true }
    
    
    如果 Generator 函数内部有 try...finally 代码块,且正在执行 try 代码块,那么 return 方法会导致立刻进入 finally 代码块,执行完以后,整个函数才会结束。
    function* numbers () {
      yield 1;
      try {
        yield 2;
        yield 3;
      } finally {
        yield 4;
        yield 5;
      }
      yield 6;
    }
    var g = numbers();
    g.next() // { value: 1, done: false }
    g.next() // { value: 2, done: false }
    g.return(7) // { value: 4, done: false }
    g.next() // { value: 5, done: false }
    g.next() // { value: 7, done: true }
    
    
    上面代码中,调用 return() 方法后,就开始执行 finally 代码块,不执行 try 里面剩下的代码了,然后等到 finally 代码块执行完,再返回 return() 方法指定的返回值。
  • next()、throw()、return() 的共同点

    next()throw()return() 这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式。
    next() 是将 yield 表达式替换成一个值。
    const g = function* (x, y) {
      let result = yield x + y;
      return result;
    };
    
    const gen = g(1, 2);
    gen.next(); // Object {value: 3, done: false}
    
    gen.next(1); // Object {value: 1, done: true}
    // 相当于将 let result = yield x + y
    // 替换成 let result = 1;
    
    
    上面代码中,第二个 next(1) 方法就相当于将 yield 表达式替换成一个值 1。如果 next() 方法没有参数,就相当于替换成 undefined
    throw() 是将 yield 表达式替换成一个 throw 语句。
    gen.throw(new Error('出错了')); // Uncaught Error: 出错了
    // 相当于将 let result = yield x + y
    // 替换成 let result = throw(new Error('出错了'));
    
    
    return() 是将 yield 表达式替换成一个 return 语句。
    gen.return(2); // Object {value: 2, done: true}
    // 相当于将 let result = yield x + y
    // 替换成 let result = return 2;
    
    
  • yield* 表达式

    如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。
    function* foo() {
      yield 'a';
      yield 'b';
    }
    
    function* bar() {
      yield 'x';
      // 手动遍历 foo()
      for (let i of foo()) {
        console.log(i);
      }
      yield 'y';
    }
    
    for (let v of bar()){
      console.log(v);
    }
    // x
    // a
    // b
    // y
    
    
    上面代码中,foobar 都是 Generator 函数,在 bar 里面调用 foo,就需要手动遍历 foo。如果有多个 Generator 函数嵌套,写起来就非常麻烦。
    ES6 提供了 yield* 表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
    function* bar() {
      yield 'x';
      yield* foo();
      yield 'y';
    }
    
    // 等同于
    function* bar() {
      yield 'x';
      yield 'a';
      yield 'b';
      yield 'y';
    }
    
    // 等同于
    function* bar() {
      yield 'x';
      for (let v of foo()) {
        yield v;
      }
      yield 'y';
    }
    
    for (let v of bar()){
      console.log(v);
    }
    // "x"
    // "a"
    // "b"
    // "y"
    
    
    再来看一个对比的例子。
    function* inner() {
      yield 'hello!';
    }
    
    function* outer1() {
      yield 'open';
      yield inner();
      yield 'close';
    }
    
    var gen = outer1()
    gen.next().value // "open"
    gen.next().value // 返回一个遍历器对象
    gen.next().value // "close"
    
    function* outer2() {
      yield 'open'
      yield* inner()
      yield 'close'
    }
    
    var gen = outer2()
    gen.next().value // "open"
    gen.next().value // "hello!"
    gen.next().value // "close"
    
    
    上面例子中,outer2 使用了 yield*outer1 没使用。结果就是,outer1 返回一个遍历器对象,outer2 返回该遍历器对象的内部值。
    从语法角度看,如果 yield 表达式后面跟的是一个遍历器对象,需要在 yield 表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为 yield* 表达式。
    let delegatedIterator = (function* () {
      yield 'Hello!';
      yield 'Bye!';
    }());
    
    let delegatingIterator = (function* () {
      yield 'Greetings!';
      yield* delegatedIterator;
      yield 'Ok, bye.';
    }());
    
    for(let value of delegatingIterator) {
      console.log(value);
    }
    // "Greetings!
    // "Hello!"
    // "Bye!"
    // "Ok, bye."
    
    
    上面代码中,delegatingIterator 是代理者,delegatedIterator 是被代理者。由于 yield* delegatedIterator 语句得到的值,是一个遍历器,所以要用星号表示。运行结果就是使用一个遍历器,遍历了多个 Generator 函数,有递归的效果。
    yield* 后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个 for...of 循环。
    function* concat(iter1, iter2) {
      yield* iter1;
      yield* iter2;
    }
    
    // 等同于
    
    function* concat(iter1, iter2) {
      for (var value of iter1) {
        yield value;
      }
      for (var value of iter2) {
        yield value;
      }
    }
    
    
    上面代码说明,yield* 后面的 Generator 函数(没有return语句时),不过是 for...of 的一种简写形式,完全可以用后者替代前者。反之,在有 return 语句时,则需要用 var value = yield* iterator 的形式获取 return 语句的值。
    如果 yield* 后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。
    function* gen(){
      yield* ["a", "b", "c"];
    }
    
    gen().next() // { value:"a", done:false }
    
    
    上面代码中,yield 命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。
    实际上,任何数据结构只要有 Iterator 接口,就可以被 yield* 遍历。
    let read = (function* () {
      yield 'hello';
      yield* 'hello';
    })();
    
    read.next().value // "hello"
    read.next().value // "h"
    
    
    上面代码中,yield 表达式返回整个字符串,yield* 语句返回单个字符。因为字符串具有 Iterator 接口,所以被 yield* 遍历。
    如果被代理的 Generator 函数有 return 语句,那么就可以向代理它的 Generator 函数返回数据。
    function* foo() {
      yield 2;
      yield 3;
      return "foo";
    }
    
    function* bar() {
      yield 1;
      var v = yield* foo();
      console.log("v: " + v);
      yield 4;
    }
    
    var it = bar();
    
    it.next()
    // {value: 1, done: false}
    it.next()
    // {value: 2, done: false}
    it.next()
    // {value: 3, done: false}
    it.next();
    // "v: foo"
    // {value: 4, done: false}
    it.next()
    // {value: undefined, done: true}
    
    
    上面代码在第四次调用 next() 方法的时候,屏幕上会有输出,这是因为函数 fooreturn 语句,向函数 bar 提供了返回值。
  • 作为对象属性的 Generator 函数

    如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
    let obj = {
      * myGeneratorMethod() {
        ···
      }
    };
    
    
    上面代码中,myGeneratorMethod 属性前面有一个星号,表示这个属性是一个 Generator 函数。
    它的完整形式如下,与上面的写法是等价的。
    let obj = {
      myGeneratorMethod: function* () {
        // ···
      }
    };
    
    
  • Generator 函数的this

    Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的 prototype 对象上的方法。
    function* g() {}
    
    g.prototype.hello = function () {
      return 'hi!';
    };
    
    let obj = g();
    
    obj instanceof g // true
    obj.hello() // 'hi!'
    
    
    上面代码表明,Generator 函数 g 返回的遍历器 obj,是 g 的实例,而且继承了 g.prototype。但是,如果把 g 当作普通的构造函数,并不会生效,因为 g 返回的总是遍历器对象,而不是 this 对象。
    function* g() {
      this.a = 11;
    }
    
    let obj = g();
    obj.next();
    obj.a // undefined
    
    
    上面代码中,Generator 函数 gthis 对象上面添加了一个属性 a,但是 obj 对象拿不到这个属性。
    Generator 函数也不能跟 new 命令一起用,会报错。
    function* F() {
      yield this.x = 2;
      yield this.y = 3;
    }
    
    new F()
    // TypeError: F is not a constructor
    
    
    上面代码中,new 命令跟构造函数 F 一起使用,结果报错,因为 F 不是构造函数。
    那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this?
    下面是一个变通方法。首先,生成一个空对象,使用 call 方法绑定 Generator 函数内部的 this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。
    function* F() {
      this.a = 1;
      yield this.b = 2;
      yield this.c = 3;
    }
    var obj = {};
    var f = F.call(obj);
    
    f.next();  // Object {value: 2, done: false}
    f.next();  // Object {value: 3, done: false}
    f.next();  // Object {value: undefined, done: true}
    
    obj.a // 1
    obj.b // 2
    obj.c // 3
    
    
    上面代码中,首先是 F 内部的 this 对象绑定 obj 对象,然后调用它,返回一个 Iterator 对象。这个对象执行三次 next() 方法(因为F内部有两个yield表达式),完成 F 内部所有代码的运行。这时,所有内部属性都绑定在 obj 对象上了,因此 obj 对象也就成了 F 的实例。
    上面代码中,执行的是遍历器对象 f,但是生成的对象实例是 obj,有没有办法将这两个对象统一呢?
    一个办法就是将 obj 换成 F.prototype
    function* F() {
      this.a = 1;
      yield this.b = 2;
      yield this.c = 3;
    }
    var f = F.call(F.prototype);
    
    f.next();  // Object {value: 2, done: false}
    f.next();  // Object {value: 3, done: false}
    f.next();  // Object {value: undefined, done: true}
    
    f.a // 1
    f.b // 2
    f.c // 3
    
    
    再将 F 改成构造函数,就可以对它执行 new 命令了。
    function* gen() {
      this.a = 1;
      yield this.b = 2;
      yield this.c = 3;
    }
    
    function F() {
      return gen.call(gen.prototype);
    }
    
    var f = new F();
    
    f.next();  // Object {value: 2, done: false}
    f.next();  // Object {value: 3, done: false}
    f.next();  // Object {value: undefined, done: true}
    
    f.a // 1
    f.b // 2
    f.c // 3
    
    
  • Generator 与状态机

    Generator 是实现状态机的最佳结构。比如,下面的 clock 函数就是一个状态机。
    var ticking = true;
    var clock = function() {
      if (ticking)
        console.log('Tick!');
      else
        console.log('Tock!');
      ticking = !ticking;
    }
    
    
    上面代码的 clock 函数一共有两种状态(Tick和Tock),每运行一次,就改变一次状态。这个函数如果用 Generator 实现,就是下面这样。
    var clock = function* () {
      while (true) {
        console.log('Tick!');
        yield;
        console.log('Tock!');
        yield;
      }
    };
    
    
    上面的 Generator 实现与 ES5 实现对比,可以看到少了用来保存状态的外部变量 ticking,这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。Generator 之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。
  • 应用场景

    (1)异步操作的同步化表达
    Generator 函数的暂停执行的效果,意味着可以把异步操作写在 yield 表达式里面,等到调用 next 方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在 yield 表达式下面,反正要等到调用 next 方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
    function* loadUI() {
      showLoadingScreen();
      yield loadUIDataAsynchronously();
      hideLoadingScreen();
    }
    var loader = loadUI();
    // 加载UI
    loader.next()
    
    // 卸载UI
    loader.next()
    
    
    上面代码中,第一次调用 loadUI 函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用 next 方法,则会显示 Loading 界面(showLoadingScreen),并且异步加载数据(loadUIDataAsynchronously)。等到数据加载完成,再一次使用 next方法,则会隐藏 Loading 界面。可以看到,这种写法的好处是所有 Loading 界面的逻辑,都被封装在一个函数,按部就班非常清晰。
    Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
    function* main() {
      var result = yield request("http://some.url");
      var resp = JSON.parse(result);
        console.log(resp.value);
    }
    
    function request(url) {
      makeAjaxCall(url, function(response){
        it.next(response);
      });
    }
    
    var it = main();
    it.next();
    
    
    上面代码的 main 函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个 yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall 函数中的 next 方法,必须加上 response 参数,因为 yield 表达式,本身是没有值的,总是等于 undefined。
    下面是另一个例子,通过 Generator 函数逐行读取文本文件。
    function* numbers() {
      let file = new FileReader("numbers.txt");
      try {
        while(!file.eof) {
          yield parseInt(file.readLine(), 10);
        }
      } finally {
        file.close();
      }
    }
    
    
    上面代码打开文本文件,使用 yield 表达式可以手动逐行读取文件。
    (2)控制流管理
    如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。
    step1(function (value1) {
      step2(value1, function(value2) {
        step3(value2, function(value3) {
          step4(value3, function(value4) {
            // Do something with value4
          });
        });
      });
    });
    
    
    采用 Promise 改写上面的代码。
    Promise.resolve(step1)
      .then(step2)
      .then(step3)
      .then(step4)
      .then(function (value4) {
        // Do something with value4
      }, function (error) {
        // Handle any error from step1 through step4
      })
      .done();
    
    
    上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。
    function* longRunningTask(value1) {
      try {
        var value2 = yield step1(value1);
        var value3 = yield step2(value2);
        var value4 = yield step3(value3);
        var value5 = yield step4(value4);
        // Do something with value4
      } catch (e) {
        // Handle any error from step1 through step4
      }
    }
    
    
    然后,使用一个函数,按次序自动执行所有步骤。
    scheduler(longRunningTask(initialValue));
    function scheduler(task) {
      var taskObj = task.next(task.value);
      // 如果Generator函数未结束,就继续调用
      if (!taskObj.done) {
        task.value = taskObj.value
        scheduler(task);
      }
    }
    
    
    注意,上面这种做法,只适合同步操作,即所有的 task 都必须是同步的,不能有异步操作。因为这里的代码一得到返回值,就继续往下执行,没有判断异步操作何时完成。如果要控制异步的操作流程,详见后面的《Generator 函数的异步应用》一章。
    下面,利用 for...of 循环会自动依次执行 yield 命令的特性,提供一种更一般的控制流管理的方法。
    let steps = [step1Func, step2Func, step3Func];
    function* iterateSteps(steps){
      for (var i=0; i< steps.length; i++){
        var step = steps[i];
        yield step();
      }
    }
    
    
    上面代码中,数组 steps 封装了一个任务的多个步骤,Generator 函数 iterateSteps 则是依次为这些步骤加上 yield 命令。
    将任务分解成步骤之后,还可以将项目分解成多个依次执行的任务。
    let jobs = [job1, job2, job3];
    
    function* iterateJobs(jobs){
      for (var i=0; i< jobs.length; i++){
        var job = jobs[i];
        yield* iterateSteps(job.steps);
      }
    }
    
    
    上面代码中,数组 jobs 封装了一个项目的多个任务,Generator 函数 iterateJobs 则是依次为这些任务加上 yield* 命令。
    最后,就可以用 for...of 循环一次性依次执行所有任务的所有步骤。
    for (var step of iterateJobs(jobs)){
      console.log(step.id);
    }
    
    
    再次提醒,上面的做法只能用于所有步骤都是同步操作的情况,不能有异步操作的步骤。如果想要依次执行异步的步骤,必须使用后面的《Generator 函数的异步应用》一章介绍的方法。
    for...of 的本质是一个 while 循环,所以上面的代码实质上执行的是下面的逻辑。
    var it = iterateJobs(jobs);
    var res = it.next();
    
    while (!res.done){
      var result = res.value;
      // ...
      res = it.next();
    }
    
    
    (3)部署 Iterator 接口
    利用 Generator 函数,可以在任意对象上部署 Iterator 接口。
    function* iterEntries(obj) {
      let keys = Object.keys(obj);
      for (let i=0; i < keys.length; i++) {
        let key = keys[i];
        yield [key, obj[key]];
      }
    }
    
    let myObj = { foo: 3, bar: 7 };
    
    for (let [key, value] of iterEntries(myObj)) {
      console.log(key, value);
    }
    
    // foo 3
    // bar 7
    
    
    上述代码中,myObj 是一个普通对象,通过 iterEntries 函数,就有了 Iterator 接口。也就是说,可以在任意对象上部署 next 方法。
    下面是一个对数组部署 Iterator 接口的例子,尽管数组原生具有这个接口。
    function* makeSimpleGenerator(array){
      var nextIndex = 0;
    
      while(nextIndex < array.length){
        yield array[nextIndex++];
      }
    }
    
    var gen = makeSimpleGenerator(['yo', 'ya']);
    
    gen.next().value // 'yo'
    gen.next().value // 'ya'
    gen.next().done  // true
    
    
    (4)作为数据结构
    Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。
    function* doStuff() {
      yield fs.readFile.bind(null, 'hello.txt');
      yield fs.readFile.bind(null, 'world.txt');
      yield fs.readFile.bind(null, 'and-such.txt');
    }
    
    
    上面代码就是依次返回三个函数,但是由于使用了 Generator 函数,导致可以像处理数组那样,处理这三个返回的函数。
    for (task of doStuff()) {
      // task是一个函数,可以像回调函数那样使用它
    }
    
    
    实际上,如果用 ES5 表达,完全可以用数组模拟 Generator 的这种用法。
    function doStuff() {
      return [
        fs.readFile.bind(null, 'hello.txt'),
        fs.readFile.bind(null, 'world.txt'),
        fs.readFile.bind(null, 'and-such.txt')
      ];
    }
    
    
    上面的函数,可以用一模一样的 for...of 循环处理!两相一比较,就不难看出 Generator 使得数据或者操作,具备了类似数组的接口。