美文网首页
前端需要掌握的ES6生成器知识

前端需要掌握的ES6生成器知识

作者: Weastsea | 来源:发表于2021-09-14 23:27 被阅读0次

    前言

    很多同学在第一次听到生成器这个概念的时候,总觉得是前端高大上的东西,可能现在依然有很多前端同学不理解这个概念,今天就从几个最常用的场景入手,来解析下生成器的应用。

    相关概念解释

    我们看了很多书和文章,都会说生成器Generator 函数是一个状态机,封装了多个内部状态。Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。 看到这个生成器的定义,我也感觉完全懵逼。我们把这个定义画重点。通过一个个简单的例子和概念的解释,争取一字一句的搞懂。

    这里先解释下什么是 遍历器对象?
    要解释这个东西,我们先从平时能理解的简单概念出发, 我们平时写代码遍历对象的时候,是不是可以通过 for ...of来处理。例如 StringArrayTypedArrayMapSet 均可以通过 for ...of 来遍历。很人多会认为理所当然,有没有深层的去思考会这样呢? 是什么黑魔法实现了遍历呢?其实有心的朋友,只要在网上搜索一下,就可以得到想要的答案。这里的遍历器对象其实是实现了ES6的定制的一组补充规范,迭代器协议

    那么接着我们学习下,什么是迭代器协议?我们看下MDN的定义

    迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。

    近一步讲只有实现了一个next()方法, 才能成为一个迭代器, 这个next 方法返回 拥有2个属性的一个对象。

    • done 表达这个迭代器是否将当前迭代的序列迭代完成,如果迭代完成,则返回true。否则返回false。
    • value 每次迭代过程中的值,迭代完成了,就返回undefined.

    这么按照原理讲,太枯燥了,具体的还是去MDN上详细学习吧。我们还是从代码中学习的快一些。结合代码去理解原理。

    实现一个生成迭代器对象的函数

    function makeIterator(array) {
        let nextIndex = 0;
        // 返回的对象是一个迭代器对象。拥有一个next方法,next方法执行后返回一个{value:, done: } 形式的对象,这样就实现了迭代器协议。
        return {
           next: function () {
               return nextIndex < array.length ? {
                   value: array[nextIndex++],
                   done: false
               } : {
                   done: true
               };
           }
        };
    }
    
    let it = makeIterator(['哟', '呀']);
    
    console.log(it.next().value); // '哟'
    console.log(it.next().value); // '呀'
    console.log(it.next().done);  // true
    
    

    通过上面的简单解释,我们应该理解了什么是遍历器对象(跟 迭代器对象是同一个意思),只有实现了迭代器协议的,都是一个迭代器对象。 很显然,生成器实现了迭代器协议。所以生成器执行后,返回的是一个迭代器对象。我们可以通过next方法来遍历。

    生成器

    接下来我们进入正题,开始我们的生成器之旅
    简单先解释下生成器的2个特征

    • function关键字与函数名之间有一个星号
    • 函数体内部使用yield表达式,定义不同的内部状态
    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
      
    // hw  返回的是一个迭代器,因为 helloWorldGenerator实现了迭代器协议,这里还懵逼的,可以耐心看看上面的解释
    var hw = helloWorldGenerator(); 
    

    执行的结果是这样的:

    hw.next()
    // { value: 'hello', done: false }
    
    hw.next()
    // { value: 'world', done: false }
    
    hw.next()
    // { value: 'ending', done: true }
    
    hw.next()
    // { value: undefined, done: true }
    

    也就是生成器底层实现了迭代器协议。yield 后面的值,是每次next函数返回的value值。
    这是我们需要理解的第一点,也是最重要的一点。

    接来下了,我们看另一个重要的例子:

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

    通过以上代码,我们引出另一个重要的概念, 可迭代协议
    其实迭代协议可以分为2个协议: 可迭代协议迭代器协议。迭代器协议是上面介绍过的。那什么是可迭代协议呢?
    我们就不粘贴定义了,这里直接看看可迭代协议如果应用于一个对象,是对象成为可迭代对象呢?

    要成为可迭代对象, 一个对象必须实现 **@@iterator** 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性。当一个对象需要被迭代的时候(比如被置入一个 for...of 循环时),首先,会不带参数调用它的 @@iterator 方法,然后使用此方法返回的迭代器获得要迭代的值。

    我们圈一下重点

    通过以上概念的讲解,我们在回过头来看👆 上面这段代码:
    Generator 函数就是迭代器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具可遍历。一个对象可遍历其实是实现了Iterator 接口。

    一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。

    这里我们又引出了新的概念。Iterator接口,其实Iterator接口也是我们的老朋友。

    ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)

    读到这里的你,是否可以把 Iterator 接口与可迭代对象和迭代器对象之前的关系梳理清楚呢?

    我个人的理解总结一下,Iterator 接口 是一个抽象的概念,一个对象实现了Iterator 接口,大白话就是:这个对象有一个Symbol.iterator属性,属性值是一个函数,函数返回的是一个迭代器对象。 这样是不是把之前支离破碎的知识给串起来了。Iterator 接口主要供for...of消费。

    原生具备 Iterator 接口的数据结构如下。

    Array
    Map
    Set
    String
    TypedArray
       - 函数的 arguments 对象
       - NodeList 对象
    

    好了,解释了这些概念,我们回归正题,继续学习生成器的概念😅。

    function* gen(){
      // some code
    }
    
    var g = gen();
    
    g[Symbol.iterator]() === g
    

    上面代码中,gen是一个 Generator 函数,调用它会生成一个遍历器对象g。它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己。这一点比较特殊。

    我们再看一个例子

    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 }
    

    这里先丢出一个结论,然后根据这个结论,我们推导下(结论最好背下来)

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

    重点再次圈下:

    • yield表达式本身没有返回值
    • next方法的一个参数是作为上一个yield的返回值。

    我们通过上面的代码具体分析下:

    // x = 5
    var a = foo(5);
    // 第一次迭代返回 x+1 为 6 
    a.next() // Object{value:6, done:false}
    // 第二次迭代 y 为undefined, 所以 undefined / 3 是 NaN
    a.next() // Object{value:NaN, done:false}
    // 第三次迭代 y 为undefined, 所以z是 NaN, x是6 ,所以加起来以后也是 NaN
    a.next() // Object{value:NaN, done:true}
    
    // x = 5
    var b = foo(5);
    //  第一次迭代返回 x+1 为 6 
    b.next() // { value:6, done:false }
    // 注意,next 传入参数12, 12 被赋予 上一个yield的返回值,也就是(yield (x + 1)) 的返回值。此时 y = 2 * 12 = 24
    b.next(12) // { value:8, done:false }
    // next 传入参数13,  13  被赋予 上一个yield的返回值 z, 所以最后返回的x+y+z =5 + 24 + 13 = 42
    b.next(13) // { value:42, done:true }
    

    更进一步的学习可以参考下面的资料。

    参考资料:

    相关文章

      网友评论

          本文标题:前端需要掌握的ES6生成器知识

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