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

前言

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

相关概念解释

我们看了很多书和文章,都会说生成器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 方法,然后使用此方法返回的迭代器获得要迭代的值。

我们圈一下重点

  • 可迭代对象要有一个 Symbol.iterator 属性。
  • Symbol.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 }

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

参考资料:

  • ECMAScript 6 入门 Generator 函数的语法
  • MDN - 迭代器协议

你可能感兴趣的:(前端需要掌握的ES6生成器知识)