美文网首页
彻底弄懂 ES6 的 Generator

彻底弄懂 ES6 的 Generator

作者: 前端好有趣 | 来源:发表于2021-06-25 09:08 被阅读0次

概念

  • Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
  • Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象 xxx,可以使用 xxx.next() 依次遍历 Generator 函数内部的每一个状态。
  1. 特征

    • function 关键字与函数名之间有一个星号(不能用于箭头函数)。

    • 函数体内部使用 yield 表达式,定义不同的内部状态。

    function* generatorTest() {
      yield 'test';
      return 'ending';
    }
    
    // 调用这个方法,返回一个 迭代器
    const gt = generatorTest();
    
  2. 调用

    gt.next() // { value: 'detanx', done: false }
    gt.next() // { value: 'ending', done: true }
    gt.next() // { value: undefined, done: true }
    
    • 第一次调用,Generator 函数开始执行,直到遇到第一个 yield 表达式为止。next 方法返回一个对象,它的 value 属性就是当前 yield 表达式的值 hellodone 属性的值 false ,表示遍历还没有结束。

    • 第二次调用,Generator 函数从上次 yield 表达式停下的地方,一直执行到 return 语句(如果没有 return 语句,就执行到函数结束)。done 属性的值 true,表示遍历已经结束。

    • 第三次调用,此时 Generator 函数已经运行完毕,next 方法返回对象的 value 属性为 undefineddone 属性为 true。以后再调用 next 方法,返回的都是这个值。

  3. 写法

    • function 关键字与函数名之间的 * 未规定,所以有很多写法,我们写得时候最好还是使用第一种,即 * 紧跟着 function 关键字后面,* 后面再加一个空格。

      function* foo(x, y) { ··· }
      

yield 表达式

  • Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield 表达式就是暂停标志。
  1. next 方法的运行逻辑

    • 遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回对象的 value 属性值。
    • 下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式。
    • 如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面表达式的值,作为返回对象的 value 属性值。
    • 如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined
  2. 定义 Generator 函数的时候, yield 后面的表达式不会立即执行,只有当调用 next 方法、内部指针指向该语句时才会执行。下面的 123 + 456 不会立即求值,只有当执行 next 到对应的 yield 表达式才会求值。

    function* gen() {
      yield  123 + 456;
    }
    
  3. yield 表达式只能用在 Generator 函数里面,用在其他地方都会报错。

    (function (){
      yield 1; // SyntaxError: Unexpected number
    })()
    
  4. yield 表达式如果用在另一个表达式之中,必须放在圆括号里面。用作 函数参数 或放在 赋值表达式的右边 ,可以不加括号。

    function* demo() {
      console.log('Hello' + yield); // SyntaxError
      console.log('Hello' + yield 123); // SyntaxError
    
      console.log('Hello' + (yield)); // OK
      console.log('Hello' + (yield 123)); // OK
    }
    // 参数和表达式右边
    function* demo() {
      foo(yield 'a', yield 'b'); // OK
      let input = yield; // OK
    }
    

与 Iterator 接口的关系

  1. Iterator 接口的关系

    • 任意一个对象的 Symbol.iterator 方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

    • Generator 函数就是遍历器生成函数,可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口。

      var myIterable = {};
      myIterable[Symbol.iterator] = function* () {
        yield 1;
        yield 2;
        yield 3;
        return 4;
      };
      [...myIterable] // [1, 2, 3],遍历只会遍历 yield 后面的值
      
  2. Generator 函数执行后,返回一个遍历器对象。该对象本身也具有 Symbol.iterator 属性,执行后返回自身。

    function* gen(){
      // some code
    }
    var g = gen();
    g[Symbol.iterator]() === g
    // true
    
  3. for...of 循环

    • for...of 循环可以自动遍历 Generator 函数运行时生成的 Iterator 对象,且此时不再需要调用 next 方法。一旦 next 方法的返回对象的 done 属性为 truefor...of 循环就会中止,且不包含该返回对象。

      function* numbers() {
        yield 1;
        yield 2;
        return 3;
      }
      for (let v of numbers()) {
        console.log(v);
      }
      // 1 2
      
  4. 其它

    • 扩展运算符(...)、解构赋值和 Array.from 方法内部调用的,都是遍历器接口。它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。

      function* numbers() {
        yield 1;
        yield 2;
        return 3;
      }
      
      // 扩展运算符
      [...numbers()] // [1, 2]
      
      // Array.from 方法
      Array.from(numbers()) // [1, 2]
      
      // 解构赋值
      let [x, y] = numbers();
      x // 1
      y // 2
      

next 方法的参数

  • yield 表达式本身没有返回值,或者说总是返回 undefinednext 方法可以带一个参数,该参数就会被当作 上一个 yield 表达式的返回值

    const g = function* (x, y) {
      let result = yield x + y;
      return result;
    };
    
    const gen = g(1, 2);
    gen.next(); // {value: 3, done: false}
    gen.next(1); // {value: 1, done: true}
    // 相当于将 let result = yield x + y
    // 替换成 let result = 1;
    
  • 由于 next 方法的参数表示上一个 yield 表达式的返回值,所以在第一次使用 next 方法时,传递参数是无效的。

    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 } 42 = 5 + 24 + 13
    

throw()、return()

共同点

  • 它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式。
  1. throw() 是将 yield 表达式替换成一个 throw 语句。

    const g = function* (x, y) {
      let result = yield x + y;
      return result;
    };
    const gen = g(1, 2);
    
    gen.throw(new Error('出错了')); // Uncaught Error: 出错了
    // 相当于将 let result = yield x + y
    // 替换成 let result = throw(new Error('出错了'));
    
  2. return() 是将 yield 表达式替换成一个 return 语句。

    gen.return(2); // {value: 2, done: true}
    // 相当于将 let result = yield x + y
    // 替换成 let result = return 2;
    

不同点

  1. Generator.prototype.throw()

    • Generator 函数返回的遍历器对象,throw 方法可以在函数体外抛出错误,然后在 Generator 函数体内捕获。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 语句捕获。

    • 如果 Generator 函数内部没有部署 try...catch 代码块,那么 throw 方法抛出的错误,将被外部 try...catch 代码块捕获。

      var g = function* () {
        while (true) {
          yield;
          // 找不到这里的 e,报错
          console.log('内部捕获',e); 
        }
      };
      
      var i = g();
      i.next();
      
      try {
        i.throw('a');
        i.throw('b');
      } catch (e) {
        console.log('外部捕获', e);
      }
      // 外部捕获 a
      
    • 如果 Generator 函数内部和外部,都没有部署 try...catch 代码块,那么程序将报错,直接中断执行。

      var gen = function* gen(){
        yield console.log('hello');
        yield console.log('world');
      }
      
      var g = gen();
      g.next(); // hello
      g.throw(); // Uncaught undefined
      
    • throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next方法。 g.throw(1) 执行时,next 方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。

      function* gen() {
        try {
          yield 1;
        } catch (e) {
          console.log('内部捕获');
        }
      }
      
      var g = gen();
      g.throw(1);
      // Uncaught 1
      
    • Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的 catch 捕获。

      function* foo() {
        var x = yield 3;
        // 数值是没有 toUpperCase 方法的,所以会抛出一个 TypeError 错误,被函数体外的catch捕获。
        var y = x.toUpperCase();
        yield y;
      }
      var it = foo();
      it.next(); // { value:3, done:false }
      try {
        it.next(42);
      } catch (err) {
        console.log(err);
      }
      
  2. Generator.prototype.return()

    • Generator 函数返回的遍历器对象,return 方法可以返回给定的值,并且终结遍历 Generator 函数。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 参数为 7,所以 value 为 7
      

yield* 表达式

  • yield* 表达式用来在一个 Generator 函数里面执行另一个 Generator 函数。

    function* foo() {
      yield 'a';
      yield 'b';
    }
    function* bar() {
      yield 'x';
      yield* foo();
      yield 'y';
    }
    for (let v of bar()){
      console.log(v);
    }
    // "x"
    // "a"
    // "b"
    // "y"
    
  • 如果 yield* 后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。yield 命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。

    function* gen(){
      yield* ["a", "b", "c"];
      yield 1
      return 'end'
    }
    let g = gen()
    g.next() // { value:"a", done:false }
    g.next() // { value:"b", done:false }
    g.next() // { value:"c", done:false }
    g.next() // {value: 1, done: false}
    g.next() // {value: "end", done: true}
    
  • 任何数据结构只要有 Iterator 接口,就可以被 yield* 遍历。

    let read = (function* () {
      yield 'hello';
      yield* 'hello';
    })();
    
    read.next().value // "hello"
    read.next().value // "h"
    read.next().value // "e"
    read.next().value // "l"
    read.next().value // "l"
    read.next().value // "o"
    read.next().value // undefined
    
  • 如果被代理的 Generator 函数有return 语句,那么就可以向代理它的 Generator 函数返回数据。下面例子中函数 fooreturn 语句,向函数 bar 提供了返回值。

    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}
    
    
    
    

作为对象属性的 Generator 函数写法

let obj = {
  * myGeneratorMethod() {
    ···
  }
};
// 等价于
let obj = {
  // *位置可以在 function 关键字和括号之间任意位置
  myGeneratorMethod: function* () {
    // ···
  }
};

Generator 函数的 this

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 对象。

  • Generator 函数也不能跟 new 命令一起用,会报错。

    function* F() {
      yield this.x = 2;
      yield this.y = 3;
    }
    
    new F()   // TypeError: F is not a constructor
    
    • 变通方法。首先,生成一个空对象,使用 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,但是生成的对象实例是 obj,将 obj 换成 F.prototype。再将 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
      

应用

  1. 取出嵌套数组的所有成员

    function* iterTree(tree) {
      if (Array.isArray(tree)) {
        for(let i=0; i < tree.length; i++) {
          yield* iterTree(tree[i]);
        }
      } else {
        yield tree;
      }
    }
    
    const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
    
    for(let x of iterTree(tree)) {
      console.log(x);
    }
    // a
    // b
    // c
    // d
    // e
    
  2. 遍历完全二叉树

    // 下面是二叉树的构造函数,
    // 三个参数分别是左树、当前节点和右树
    function Tree(left, label, right) {
      this.left = left;
      this.label = label;
      this.right = right;
    }
    
    // 下面是中序(inorder)遍历函数。
    // 由于返回的是一个遍历器,所以要用generator函数。
    // 函数体内采用递归算法,所以左树和右树要用yield*遍历
    function* inorder(t) {
      if (t) {
        yield* inorder(t.left);
        yield t.label;
        yield* inorder(t.right);
      }
    }
    
    // 下面生成二叉树
    function make(array) {
      // 判断是否为叶节点
      if (array.length == 1) return new Tree(null, array[0], null);
      return new Tree(make(array[0]), array[1], make(array[2]));
    }
    let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
    
    // 遍历二叉树
    var result = [];
    for (let node of inorder(tree)) {
      result.push(node);
    }
    
    result
    // ['a', 'b', 'c', 'd', 'e', 'f', 'g']
    
  3. Ajax 的异步操作

    • 通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式,本身是没有值的,总是等于undefined
    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();
    
  4. 逐行读取文本文件。

    function* numbers() {
      let file = new FileReader("numbers.txt");
      try {
        while(!file.eof) {
          yield parseInt(file.readLine(), 10);
        }
      } finally {
        file.close();
      }
    }
    
  5. 控制流管理

    • 如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。
    step1(function (value1) {
      step2(value1, function(value2) {
        step3(value2, function(value3) {
          step4(value3, function(value4) {
            // Do something with value4
          });
        });
      });
    });
    
    • 利用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();
      }
    }
    
  6. 部署 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
    
  7. 作为数据结构

    • 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');
    }
    
    for (task of doStuff()) {
      // task是一个函数,可以像回调函数那样使用它
    }
    

相关文章

网友评论

      本文标题:彻底弄懂 ES6 的 Generator

      本文链接:https://www.haomeiwen.com/subject/iapnyltx.html