美文网首页
Javascript学习笔记-生成器

Javascript学习笔记-生成器

作者: Patrick浩 | 来源:发表于2017-11-06 23:28 被阅读0次
    Javascript-生成器.png

    在Javascript中,普通函数一旦开始运行在函数运行结束之前是不会中断的,而ES6引入的Generator(生成器)可以使得函数可以发生中断,分步运行。

    1 Iterable和Iterator

    在说明如何创建和使用Generator之前,先要说到ES6中迭代相关问题。

    1.1 Iterable

    在ES6中,可以使用for of的方式来循环遍历对象:

    for(let i of [1,2,3,4]){
        console.log(i);
    }
    

    一个对象如果能够进行这样的迭代循环就被称为iterable(可迭代)。在iterable的对象中包含一个属性[Symbol.iterator],这个属性的值是一个function,而这个function是一个iterator(迭代器)

    var obj = {
        [Symbol.iterator]:function iterator(){
            // TODO 省略掉了iterator的实现
        }
    }
    

    1.2 Iterator

    一个iterator是返回指一个包含next属性,其值为是一个方法,且调用该next方法会返回对象符合{value:xx, done: xx}结构的值。

    var iterator = function() {
        let i = 0;
        return {
            next() {
                i++;
                return {value:i, done: i > 3}
            }
        }
    }
    

    1.3 ES6中的迭代

    自定义iterable对象的完整创建过程如下:

    var o = {
        [Symbol.iterator]: function() {
            let i = 0;
            return {
                next() {
                    i++;
                    return {value:i, done: i > 3}
                }
            }
        }
    }
    

    在使用for of进行迭代操作的时候,每一次循环都会调用iteratornext函数,将每一次next调用返回的value作为迭代的值,同时会一直迭代直到done的值返回true为止

    for(let i of o) {
        console.log(i); // 1, 2, 3 因为当i>3的时候done返回为true,所以就停止执行
    }
    

    同时ES6中引入了[...x](分离操作)的语法糖,可以将iterable对象迭代的结果值放到一个数组中:

    var arr = [...o]; 
    console.log(arr); // [1,2,3]
    

    2. Generator的创建

    生成器自身是iterable的,同时自身是一个iterator对象

    2.1 使用function *()创建

    生成器通常使用function *()的方式进行创建,声明中*的位置可以直接跟在function关键字后面也可以在函数名前面,创建的生成器函数中包含yield关键字。

    function *g() { // 也可以是function* g()
      yield 10;
    }
    

    2.2 使用GeneratorFunction构造方法创建

    也可以使用GeneratorFunction构造方法的形式创建生成器,不过用起来就比较复杂了,其中GeneratorFunction(...arguments, exec)接受的最后一个参数exec为生成器方法体,前面的参数为函数构造器的参数

    var GeneratorFunction = Object.getPrototypeOf(function *(){}).constructor;
    var g = new GeneratorFunction('a', 'yield a');
    // 上面的构造过程相当于
    function *g(a) {
      yield a;
    }
    

    通常使用function *()声明,因为看起来更适合阅读。

    3 Generator的使用

    3.1 获取iterator

    在使用生成器的时候,直接调用构造方法,就可以返回一个iterator(迭代器)

    function *g() {
      yield 1;
    }
    var it = g(); // 生成一个iterator
    

    之前说过Generator其自身是iterable的同时是iterator,主要表现在:

    function *g(){
      yield 1;
    }
    var it = g();
    console.log(it[Symbol.iterator]() === it);
    

    3.2 顺序化异步执行

    构造了迭代器对象iterator后,和普通的函数不同,生成器函数并没有进行执行,只有在调用了next方法(也可以是return或者throw)方法以后,才会开始执行,且按照以下方式可以判断执行结果:

    1. 执行到第一个yiled停止,返回{value: x, done: x}格式的数据,其中将yield关键字后的值作为value的值输出,并将done设置为false,再次调用next方法,函数会再次向后执行
    2. 如果调用next方法以后函数向后执行不存在yield,则此时valueundefined(或者return的值),done将设置为true
    3. 如果调用next方法以后函数向后执行存在return关键字,则此时valuereturn的值,done将设置为true

    // 生成器最后不返回值
    function *g1() {
      var a = yield 3;
      console.log(a);
    }
    var it1 = g1();
    console.log(it1.next()); // {value: 3, done: false};
    console.log(it1.next()); // {value: undefined, done: true};
    // 生成器有return语句
    function *g2() {
      var a = yield 3;
      console.log(a);
      return 2;
    }
    var it2 = g2();
    console.log(it2.next()); // {value: 3, done: false};
    console.log(it2.next()); // {value: 2, done: true};
    

    除了通过next方法可以获取到yield之后到值以外,next方法可以添加一个参数,参数将把yield表达式替换为该参数值

    function *g1() {
      var a = yield 3;
      console.log('a=' + a);
    }
    var it1 = g1();
    it1.next(); // 运行到`yield 3`停止,并返回{value: 3, done: false};
    it1.next(1); // 继续运行,同时把`yield 3`替换为1,输出生成器函数中console.log('a=' + a)语句
    

    要注意三点:

    如果调用return,迭代器会立即结束,value值为return的值,done设置为true

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

    如果生成器方法中存在return语句,执行到return语句后会将返回值作为value的值,且done会变为true,继续调用next方法,不能获取到yield的值,value始终为undefineddone始终为true

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

    如果调用迭代器的throw方法或者生成器函数中存在throw操作,将抛出异常,迭代器运行结束

    function *g() {
      yield 1;
      throw 2;
    }
    var it = g();
    console.log(it.next()); // {value: 1, done: false};
    console.log(it.next()); // 提示异常,并终止运行
    // 或者
    function *g() {
      yield 1;
      yield 2;
    }
    var it = g();
    console.log(it.throw(2)); // 提示异常,并终止运行
    console.log(it.next()); // 不会执行
    
    3.2.1 在异步方法中的使用

    生成器因为可以进行中断运行,且利用next方法控制运行,所以对于异步方法,就可以使用顺序的方式来编写代码,而不用在回调中进行数据的处理。

    // 普通方法执行
    function f() {
      var a;
      setTimeout(_=> {
        a = 3;
      }, 1000);
      var b = a * 100; 
      console.log('b=' + b);
    }
    f(); // 输出b=NaN
    // 使用生成器执行
    function *g() {
      var a = yield setTimeout(_=> {
        it.next(3);
      }, 1000);
      var b = a * 100; // 
      console.log('b=' + b);
    }
    var it = g();
    it.next(); // 1s后会输出b=300
    
    3.2.2 和Promise的联合用法

    和在异步方法中的控制过程一样,可以在Promise中使用来控制程序的执行

    function *g() {
      var p = yield Promise.resolve(3).then(fulfill => {
        setTimeout(_=>{
          it.next(fulfill);
        }, 1000)
      });
      console.log('p=' + p);
    }
    var it = g();
    it.next(); // 1s后输出 p=3
    

    3.3 [Symbol.iterator]的迭代器

    由于生成器的特性,于是用来创建iterable对象的iterator

    var obj = {
      [Symbol.iterator]: function *g() {
        let i = 0;
        while(i < 3) {
          i++;
          yield i;
        }
        return 'finish' + i;
      }
    }
    for(let o of obj) {
      console.log(o); // 1, 2, 3
    }
    

    4 其他

    4.1 形实转换程序

    在Javascript中使用一个函数来来封装其他函数,并将需要的参数也一并封装

    function f(x, y){
      return x + y;
    }
    function thunk() {
      return f(3, 4);
    }
    thunk(); // 返回7
    

    对于存在回调的函数,可以采用增加一个回调函数作为参数

    function f(x, y, cb){
      setTimeout(_=>{
        cb(x, y);
      },1000)
    }
    function thunk(cb) {
      return f(3, 4, cb);
    }
    thunk((x, y)=>{
      console.log(x+y); // 7;
    });
    

    利用这种写法,我们可以对很多异步方法进行重新封装,可以让我们的焦点放到回调函数中,可以一定程度将参数输入和输出的处理过程进行分开开发。

    4.2 生成器委托

    在生成器中,可以嵌套iterable对象,嵌套iterable对象的时候并使用yield*,当执行到yield* iterable的时候,会将运行流程交给iterable对象的iterator。并在继续执行时调用其next方法。

    function *g1() {
      yield 2;
      yield 3;
    }
    function *g2() {
      yield 1;
      yield* g1(); // 或者yield *g1();
      yield 4;
    }
    var it = g2();
    console.log(it.next()); // {value: 1, done: false};
    console.log(it.next()); // {value: 2, done: false};
    console.log(it.next()); // {value: 3, done: false};
    console.log(it.next()); // {value: 4, done: false};
    console.log(it.next()); // {value: undefined, done: true};
    

    5 总结

    生成器是一个很有意思的东西,使用生成器可以将异步操作进一步升级,用一种同步编码风格去处理异步的情况。

    6 参考

    《你不知道的Javascript(中卷)》
    MDN Generator

    相关文章

      网友评论

          本文标题:Javascript学习笔记-生成器

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