阅读 2655

ES6常用但被忽略的方法(第六弹Generator )

写在开头

  • ES6常用但被忽略的方法 系列文章,整理作者认为一些日常开发可能会用到的一些方法、使用技巧和一些应用场景,细节深入请查看相关内容连接,欢迎补充交流。

相关文章

Generator

基本概念

  • Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。语法上,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
  1. 特征
    1. function关键字与函数名之间有一个星号;
    2. 函数体内部使用yield表达式,定义不同的内部状态。
    function* detanxGenerator() {
      yield 'detanx';
      return 'ending';
    }
    
    const dg = detanxGenerator();
    复制代码
  2. 调用
    dg.next() // { value: 'detanx', done: false }
    dg.next() // { value: 'ending', done: true }
    dg.next() // { value: undefined, done: true }
    复制代码
    1. 第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hellodone属性的值false,表示遍历还没有结束。
    2. 第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。done属性的值true,表示遍历已经结束。
    3. 第三次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefineddone属性为true。以后再调用next方法,返回的都是这个值。
  3. 写法
    • function关键字与函数名之间的*未规定,所以有很多写法,我们写得时候最好还是使用第一种,即*紧跟着function关键字后面,*后面再加一个空格。
    function* foo(x, y) { ··· }
    function*foo(x, y) { ··· }
    function *foo(x, y) { ··· }
    function * foo(x, y) { ··· }
    复制代码

yield 表达式

  • Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
  1. next方法的运行逻辑
    (1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
    (2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
    (3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
    (4)如果该函数没有return语句,则返回的对象的value属性值为undefined
  2. yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行。下面的123 + 456不会立即求值,只有当执行next到对应的yield表达式才会求值。
function* gen() {
  yield  123 + 456;
}
复制代码
  1. yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
(function (){
  yield 1;
})()
// SyntaxError: Unexpected number
复制代码
  1. 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接口和for...of 循环的关系

  1. Iterator 接口的关系
  • 上一弹ES6常用但被忽略的方法(第五弹Promise和Iterator)说过,任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
  • Generator 函数就是遍历器生成函数,可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};
[...myIterable] // [1, 2, 3]
复制代码
  • Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
function* gen(){
  // some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
复制代码
  1. 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
复制代码
  1. 除了for...of循环
  • 扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口。它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。
// 扩展运算符
[...numbers()] // [1, 2]

// Array.from 方法
Array.from(numbers()) // [1, 2]

// 解构赋值
let [x, y] = numbers();
x // 1
y // 2
复制代码

next 方法的参数

  • yield表达式本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
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方法提供参数,返回结果就完全不一样了。上面代码第一次调用b的next方法时,返回x+1的值6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5y等于24,所以return语句的值等于42

  • 由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。

next()、throw()、return()

共同点
  • 它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。
  1. 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;
复制代码
  1. throw()是将yield表达式替换成一个throw语句。
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
复制代码
  1. return()是将yield表达式替换成一个return语句。
gen.return(2); // Object {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;
        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();
    g.throw();
    // hello
    // 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;
      var y = x.toUpperCase();
      yield y;
    }
    var it = foo();
    it.next(); // { value:3, done:false }
    try {
      it.next(42);
    } catch (err) {
      console.log(err);
    }
    复制代码
    • 数值是没有 toUpperCase 方法的,所以会抛出一个 TypeError 错误,被函数体外的 catch 捕获。
  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 }
    复制代码

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"];
}
gen().next() // { value:"a", done:false }
复制代码
  • 任何数据结构只要有 Iterator 接口,就可以被yield*遍历。
let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();

read.next().value // "hello"
read.next().value // "h"
复制代码
  • 如果被代理的 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 = {
  myGeneratorMethod: function* () { // *位置可以在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是一个函数,可以像回调函数那样使用它
    }
    复制代码
  8. 异步应用
文章分类
前端
文章标签