• Generator


    1 简介

    Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。
    形式上,Generator函数是一个普通函数,但是有两个特征

    • function关键字与函数名之间有一个星号
    • 函数体内部使用yield语句,定义不同的内部状态
      我们先来看一个例子:
    function* helloWorldGenerator() {
        for (var i = 5; i >= 0; i--) {
            yield i;
        }
        return 'finished';
    }
    var hw = helloWorldGenerator();
    console.log(hw.next());//{ value: 0, done: false }
    console.log(hw.next());//{ value: 1314, done: false }
    console.log(hw.next());//{ value: 5201314, done: true }
    console.log(hw.next());//{ value: undefined, done: true }
    

    可以把它理解成,Generator函数是一个状态机,封装了多个内部状态
    Generator函数的调用方法与普通函数一样。不同的是,调用Generator函数后,该函数并不执行,而是返回一个遍历器对象
    接下来,调用遍历器对象的next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止,并输出yield或return语句后面的表达式的值与是否遍历完成的标志。
    也就是说,Generator函数是分段执行的,分段的标记就是yield语句。

    2 yield

    Generator实际上提供了一种可以暂停执行的函数,yield语句就是暂停标志。
    遍历器对象的next方法的运行逻辑如下:

    1、遇到yield语句,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
    2、下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。
    3、如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
    4、如果该函数没有return语句,则返回的对象的value属性值为undefined。
    
    • yield语句后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的“惰性求值”的语法功能。
    • yield语句不能用在普通函数中,否则会报错。
    • yield语句如果用在一个表达式之中,必须放在圆括号里面。
    • yield句本身没有返回值,或者说总是返回undefined。

    3 与Iterator接口的关系

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

    var myIterable = {};
    myIterable[Symbol.iterator] = function* () {
      yield 1;
      yield 2;
      yield 3;
    };
    
    [...myIterable] // [1, 2, 3]
    

    4 next方法的参数

    yield句本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。
    Generator函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。

    function* foo(x) {
      var y = 2 * (yield (x + 1));
      var z = yield (y / 3);
      return (x + y + z);
    }
    
    var a = foo(5);
    console.log(a.next()) // Object{value:6, done:false}
    console.log(a.next()) // Object{value:NaN, done:false}
    console.log(a.next()) // Object{value:NaN, done:true}
    
    var b = foo(5);
    //这句next调用后,函数停在了yield (x + 1)这里。
    console.log(b.next()) // { value:6, done:false }
    //执行这句next时指定了参数,就意味着将yield (x + 1)的返回值设置为了12
    //继续执行时就会执行var y = 2 * 12;
    console.log(b.next(12)) // { value:8, done:false }
    console.log(b.next(13)) // { value:42, done:true }
    

    5 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
    

    一旦next方法的返回对象的done属性为true,for…of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for…of循环之中
    利用for…of循环,可以写出遍历任意对象(object)的方法。

    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
    

    6 Generator.prototype.throw()

    Generator函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获

    var g = function* () {
      try {
        yield '如果不先执行一次next就直接调用throw抛出的错误是不会被内部捕获到的,因为抛出错误时函数其实并没有进到这个try里。';
      } catch (e) {
        console.log('内部捕获1', e);
      }
    };
    
    var i = g();
    try {
        console.log(i.throw('cant catch inside'));
    } catch (e) {
      console.log('外部捕获1', e);
    }
    console.log(i.next());
    //外部捕获1 cant catch
    //Object { value: undefined, done: true }
    
    • 一旦Generator执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。
    • 如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象, 即JavaScript引擎认为这个Generator已经运行结束了。
    var g = function* () {
      try {
        yield '如果不先执行一次next就直接调用throw抛出的错误是不会被内部捕获到的,因为抛出错误时函数其实并没有进到这个try里。';
      } catch (e) {
        console.log('内部捕获1', e);
      }
      try {
        yield 'throw方法会顺便执行一次next';
      } catch (e) {
        console.log('内部捕获2', e);
      }
      return '这次的错误没有在内部捕获,就会传到外部';
    };
    
    var i = g();
    console.log(i.next());
    
    try {
      console.log(i.throw('a'));
      console.log(i.throw('b'));
      console.log(i.throw('c'));
    } catch (e) {
      console.log('外部捕获2', e);
    }
    // Object { value: "如果不先执行一次next就直接调用throw抛出的错误是不会被内部捕获…", done: false }
    //内部捕获1 a
    //Object { value: "throw方法会顺便执行一次next", done: false }
    //内部捕获2 b
    //Object { value: "这次的错误没有在内部捕获,就会传到外部", done: true }
    //外部捕获2 c
    

    7 Generator.prototype.return()

    Generator函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历Generator函数。

    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    var g = gen();
    
    console.log(g.next())        // { value: 1, done: false }
    console.log(g.return('foo')) // { value: "foo", done: true }
    console.log(g.next())        // { value: undefined, done: true }
    

    如果Generator函数内部有try…finally代码块,那么调用return方法会立即去执行finally代码块中的代码,执行完再返回return方法指定的值和循环完的标志。

    function* numbers () {
      yield 1;
      try {
        yield 2;
        yield 3;
      } finally {
        yield 4;
        yield 5;
      }
      yield 6;
    }
    var g = numbers()
    console.log(g.next()) // { done: false, value: 1 }
    console.log(g.next()) // { done: false, value: 2 }
    console.log(g.return(7)) // { done: false, value: 4 }
    console.log(g.next()) // { done: false, value: 5 }
    console.log(g.next()) // { done: true, value: 7 }
    

    8 yield*语句

    如果在Generater函数内部,调用另一个Generator函数,默认情况下是没有效果的。
    这个就需要用到yield*语句,用来在一个Generator函数里面执行另一个Generator函数。
    如果被代理的Generator函数有return语句,那么就可以向代理它的Generator函数返回数据。

    function* foo() {
      yield 'a';
      yield 'b';
      return "Generator 的返回值";
    }
    function* bar() {
      yield 'x';
      console.log(foo());//Generator对象
      console.log(yield foo());//undefined 普通的yield获取不到foo的返回值
      console.log((yield* foo())+'in');//Generator 的返回值in yield*获取的到foo的返回值,这个返回值在yield里不会再被遍历到
      yield 'y';
    }
    for (let v of bar()){
      console.log(v);
    }
    //x
    //Generator {  }
    //Generator {  }
    //undefined
    //a
    //b
    //Generator 的返回值in
    //y
    

    8.1 对于那些有原生遍历器接口的yield*可以直接遍历:

    function* gen(){
      yield* ["a", "b", "c"];
      yield ["a", "b", "c"];
      yield* 'hello';
    }
    
    for (var i of gen()) {
        console.log(i);
    }
    /*
    a
    b
    c
    Array [ "a", "b", "c" ]
    h
    e
    l
    l
    o
    */
    

    8.2 利用yield*语句取出嵌套数组

    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
    

    8.3 使用yield*语句遍历完全二叉树

    首先来看看我们的数的结构:

    // 三个参数分别是左树、当前节点和右树
    function Tree(left, label, right) {
      this.left = left;
      this.label = label;
      this.right = 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 res = [];
    function re(root) {
        if (root) {
            re(root.left);
            res.push(root.label);
            re(root.right);
        }
    }
    re(tree);
    console.log(res);
    ``` js
    使用yield遍历:
    

    // 下面是中序(inorder)遍历函数。
    // 由于返回的是一个遍历器,所以要用generator函数。
    // 函数体内采用递归算法,所以左树和右树要用yield遍历
    function
    inorder(t) {
    if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
    }
    }

    // 遍历二叉树
    var result = [];
    for (let node of inorder(tree)) {
    result.push(node);
    }

    console.log(result);
    // ['a', 'b', 'c', 'd', 'e', 'f', 'g']

    # 9 Generator函数的this
    Generator函数比较特殊,他总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的prototype对象上的方法。
    
    ## 9.1 不能读取自己的this
    但是由于Generator函数总是返回遍历器对象而不是自己本身,把这个函数当做构造函数用是不行的。
    ``` js
    function* g() {
        this.a = 1;
        console.log("done");
    }
    
    g.prototype.hello = function () {
      return 'hi!';
    };
    
    let obj = g();
    obj.next();
    
    console.log(obj instanceof g) // true
    console.log(obj.hello()) // 'hi!'
    console.log(obj.a) // 'undefined'
    

    如果你想同时使用this和遍历器可以用另一个对象绑在Generator函数的this上,如果新建一个对象来绑显然不太方便使用,将Generator函数的prototype绑上就可以在本对象上又使用this又使用遍历器了

    function* F() {
      this.a = 1;
      yield this.b = 2;
      yield this.c = 3;
    }
    var f = F.call(F.prototype);
    
    console.log(f.next());  // Object {value: 2, done: false}
    console.log(f.next());  // Object {value: 3, done: false}
    console.log(f.next());  // Object {value: undefined, done: true}
    
    console.log(f.a) // 1
    console.log(f.b) // 2
    console.log(f.c) // 3
    

    9.2 不能使用new命令

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

    function* F() {
      yield this.x = 2;
      yield this.y = 3;
    }
    
    new F()
    // TypeError: F is not a constructor
    

    你要非想用,就包一层吧:

    function* gen() {
      this.a = 1;
      yield this.b = 2;
      yield this.c = 3;
    }
    function F() {
      return gen.call(gen.prototype);
    }
    var f = new F();
    console.log(f.next());  // Object {value: 2, done: false}
    console.log(f.next());  // Object {value: 3, done: false}
    console.log(f.next());  // Object {value: undefined, done: true}
    console.log(f.a) // 1
    console.log(f.b) // 2
    console.log(f.c) // 3
    

    9.3 意义- Generator与状态机

    Generator是实现状态机的最佳结构。
    假如一个clock有两个状态:

    var clock = function*() {
      while (true) {
        console.log('Tick!');
        yield;
        console.log('Tock!');
        yield;
      }
    };
    var li = clock();
    li.next();//Tick
    li.next();//Tock
    li.next();//Tick
    li.next();//Tock
    

    如果要用ES5来实现就要有一个标志变量来保存当前的状态。
    这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。

    10 应用

    10.1 异步操作的同步化表达

    比如Ajax,我们需要等回调,把对返回数据的处理放在回调函数中,但是有了Generator就不同了

    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){
            //数据返回后将数据作为yield的返回值传到Generator里
            it.next(response);
        });
    }
    //初始化
    var it = main();
    //发起Ajax请求
    it.next();
    

    10.2 控制流管理

    如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。

    step1(function (value1) {
      step2(value1, function(value2) {
        step3(value2, function(value3) {
          step4(value3, function(value4) {
            // Do something with value4
          });
        });
      });
    });
    

    使用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
      }
    }
    function scheduler(task) {
      var taskObj = task.next(task.value);
      // 如果Generator函数未结束,就继续调用
      if (!taskObj.done) {
        task.value = taskObj.value
        scheduler(task);
      }
    }
    scheduler(longRunningTask(initialValue));
    

    或者更一般的办法:

    let steps = [step1Func, step2Func, step3Func];
    
    function *iterateSteps(steps){
      for (var i=0; i< steps.length; i++){
        var step = steps[i];
        yield step();
      }
    }
    

    10.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);
    }
    

    原文:https://blog.csdn.net/exialym/article/details/52813286

  • 相关阅读:
    asp.net 通过js调用webService注意
    身份证号码验证 类
    char值码对应大全
    C# 让textbox 只能输入数字的方法
    table嵌套循环数据
    拆分字符串
    angular的路由配置
    js对象数组(JSON) 根据某个共同字段 分组
    当前时间的后七天
    ajax提交时“加载中”提示的处理方法
  • 原文地址:https://www.cnblogs.com/qiqi715/p/10209191.html
Copyright © 2020-2023  润新知