前端需要掌握的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 方法,然后使用此方法返回的迭代器获得要迭代的值。

我们圈一下重点

通过以上概念的讲解,我们在回过头来看👆 上面这段代码:
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 }

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

参考资料:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容