前言
很多同学在第一次听到生成器这个概念的时候,总觉得是前端高大上的东西,可能现在依然有很多前端同学不理解这个概念,今天就从几个最常用的场景入手,来解析下生成器的应用。
相关概念解释
我们看了很多书和文章,都会说生成器Generator 函数是一个状态机,封装了多个内部状态。Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
看到这个生成器的定义,我也感觉完全懵逼。我们把这个定义画重点。通过一个个简单的例子和概念的解释,争取一字一句的搞懂。
这里先解释下什么是 遍历器对象?
要解释这个东西,我们先从平时能理解的简单概念出发, 我们平时写代码遍历对象的时候,是不是可以通过 for ...of
来处理。例如 String
、Array
、TypedArray
、Map
和 Set
均可以通过 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 - 迭代器协议