前言html
《你不懂js》系列又来了,很久不露面了,早读君在看的时候内心在想,这种你们运用度如何,平时工做会用到吗?文章有点长,你能够大概的了解下,今天早读文章来自前端早读课专栏做者@HetfieldJoe的分享。前端
正文从这开始~git
编写JS代码是一回事儿,而合理地组织它是另外一回事儿。利用常见的组织和重用模式在很大程度上改善了你代码的可读性和可理解性。记住:代码在与其余开发者交流上起的做用,与在给计算机喂指令上起的做用一样重要。es6
ES6拥有几种重要的特性能够显著改善这些模式,包括:迭代器,generator,模块,和类。github
迭代器算法
迭代器(iterator) 是一种结构化的模式,用于从一个信息源中以一次一个的方式抽取信息。这种模式在程序设计中存在好久了。并且不能否认的是,不知从何时起JS开发者们就已经特别地设计并实现了迭代器,因此它根本不是什么新的话题。数据库
ES6所作的是,为迭代器引入了一个隐含的标准化接口。许多在JavaScript中内建的数据结构如今都会暴露一个实现了这个标准的迭代器。并且你也能够构建本身的遵循一样标准的迭代器,来使互用性最大化。数组
迭代器是一种消费数据的方法,它是组织有顺序的,相继的,基于抽取的。缓存
举个例子,你可能实现一个工具,它在每次被请求时产生一个新的惟一的标识符。或者你可能循环一个固定的列表以轮流的方式产生一系列无限多的值。或者你能够在一个数据库查询的结果上添加一个迭代器来一次抽取一行结果。网络
虽然在JS中它们不常常以这样的方式被使用,可是迭代器还能够认为是每次控制行为中的一个步骤。这会在考虑generator时获得至关清楚的展现(参见本章稍后的“Generator”),虽然你固然能够不使用generator而作一样的事。
接口
在本书写做的时候,ES6的25.1.1.2部分 (https://people.mozilla.org/~jorendorff/es6-draft.html#sec-iterator-interface) 详述了Iterator接口,它有以下的要求:
有两个可选成员,有些迭代器用它们进行了扩展:
接口IteratorResult被规定为:
注意: 我称这些接口是隐含的,不是由于它们没有在语言规范中被明确地被说出来 —— 它们被说出来了!—— 而是由于它们没有做为能够直接访问的对象暴露给代码。在ES6中,JavaScript不支持任何“接口”的概念,因此在你本身的代码中遵循它们纯粹是惯例上的。可是,不论JS在何处须要一个迭代器 —— 例如在一个for..of循环中 —— 你提供的东西必须遵循这些接口,不然代码就会失败。
还有一个Iterable接口,它描述了必定可以产生迭代器的对象:
若是你回忆一下第二章的“内建Symbol”,@@iterator是一种特殊的内建symbol,表示能够为对象产生迭代器的方法。
IteratorResult
IteratorResult接口规定从任何迭代器操做的返回值都是这样形式的对象:
内建迭代器将老是返回这种形式的值,固然,更多的属性也容许出如今这个返回值中,若是有必要的话。
例如,一个自定义的迭代器可能会在结果对象中加入额外的元数据(好比,数据是从哪里来的,取得它花了多久,缓存过时的时间长度,下次请求的恰当频率,等等)。
注意: 从技术上讲,在值为undefined的状况下,value是可选的,它将会被认为是不存在或者是没有被设置。由于无论它是表示的就是这个值仍是彻底不存在,访问res.value都将会产生undefined,因此这个属性的存在/不存在更大程度上是一个实现或者优化(或二者)的细节,而非一个功能上的问题。
next()迭代
让咱们来看一个数组,它是一个可迭代对象,能够生成一个迭代器来消费它的值:
每一次定位在Symbol.iterator上的方法在值arr上被调用时,它都将生成一个全新的迭代器。大多数的数据结构都会这么作,包括全部内建在JS中的数据结构。
然而,像事件队列这样的结构也许只能生成一个单独的迭代器(单例模式)。或者某种结构可能在同一时间内只容许存在一个惟一的迭代器,要求当前的迭代器必须完成,才能建立一个新的。
前一个代码段中的it迭代器不会再你获得值3时报告done: true。你必须再次调用next(),实质上越过数组末尾的值,才能获得完成信号done: true。在这一节稍后会清楚地讲解这种设计方式的缘由,可是它一般被认为是一种最佳实践。
基本类型的字符串值也默认地是可迭代对象:
注意: 从技术上讲,这个基本类型值自己不是可迭代对象,但多亏了“封箱”,"hello world"被强制转换为它的String对象包装形式,它 才是一个可迭代对象。更多信息参见本系列的 类型与文法。
ES6还包括几种新的数据结构,称为集合(参见第五章)。这些集合不只自己就是可迭代对象,并且它们还提供API方法来生成一个迭代器,例如:
一个迭代器的next(..)方法可以可选地接受一个或多个参数。大多数内建的迭代器不会实施这种能力,虽然一个generator的迭代器绝对会这么作(参见本章稍后的“Generator”)。
根据通常的惯例,包括全部的内建迭代器,在一个已经被耗尽的迭代器上调用next(..)不是一个错误,而是简单地持续返回结果{ value: undefined, done: true }。
可选的return(..)和throw(..)
在迭代器接口上的可选方法 —— return(..)和throw(..) —— 在大多数内建的迭代器上都没有被实现。可是,它们在generator的上下文环境中绝对有某些含义,因此更具体的信息能够参看“Generator”。
return(..)被定义为向一个迭代器发送一个信号,告知它消费者代码已经完成并且不会再从它那里抽取更多的值。这个信号能够用于通知生产者(应答next(..)调用的迭代器)去实施一些可能的清理做业,好比释放/关闭网络,数据库,或者文件引用资源。
若是一个迭代器拥有return(..),并且发生了能够自动被解释为非正常或者提早终止消费迭代器的任何状况,return(..)就将会被自动调用。你也能够手动调用return(..)。
return(..)将会像next(..)同样返回一个IteratorResult对象。通常来讲,你向return(..)发送的可选值将会在这个IteratorResult中做为value发送回来,虽然在一些微妙的状况下这可能不成立。
throw(..)被用于向一个迭代器发送一个异常/错误信号,与return(..)隐含的完成信号相比,它可能会被迭代器用于不一样的目的。它不必定像return(..)同样暗示着迭代器的彻底中止。
例如,在generator迭代器中,throw(..)实际上会将一个被抛出的异常注射到generator暂停的执行环境中,这个异常能够用try..catch捕获。一个未捕获的throw(..)异常将会致使generator的迭代器异常停止。
注意: 根据通常的惯例,在return(..)或throw(..)被调用以后,一个迭代器就不该该在产生任何结果了。
迭代器循环
正如咱们在第二章的“for..of”一节中讲解的,ES6的for..of循环能够直接消费一个规范的可迭代对象。
若是一个迭代器也是一个可迭代对象,那么它就能够直接与for..of循环一块儿使用。经过给予迭代器一个简单地返回它自身的Symbol.iterator方法,你就可使它成为一个可迭代对象:
如今咱们就能够用一个for..of循环来消费迭代器it了:
为了彻底理解这样的循环如何工做,回忆下第二章中的for..of循环的for等价物:
若是你仔细观察,你会发现it.next()是在每次迭代以前被调用的,而后res.done才被查询。若是res.done是true,那么这个表达式将会求值为false因而此次迭代不会发生。
回忆一下以前咱们建议说,迭代器通常不该与最终预期的值一块儿返回done: true。如今你知道为何了。
若是一个迭代器返回了{ done: true, value: 42 },for..of循环将彻底扔掉值42。所以,假定你的迭代器可能会被for..of循环或它的for等价物这样的模式消费的话,你可能应当等到你已经返回了全部相关的迭代值以后才返回done: true来表示完成。
警告: 固然,你能够有意地将你的迭代器设计为将某些相关的value与done: true同时返回。但除非你将此状况在文档中记录下来,不然不要这么作,由于这样会隐含地强制你的迭代器消费者使用一种,与咱们刚才描述的for..of或它的手动等价物不一样的模式来进行迭代。
自定义迭代器
除了标准的内建迭代器,你还能够制造你本身的迭代器!全部使它们能够与ES6消费设施(例如,for..of循环和...操做符)进行互动的代价就是遵循恰当的接口。
让咱们试着构建一个迭代器,它可以以斐波那契(Fibonacci)数列的形式产生无限多的数字序列:
警告: 若是咱们没有插入break条件,这个for..of循环将会永远运行下去,这回破坏你的程序,所以可能不是咱们想要的!
方法Fib[Symbol.iterator]()在被调用时返回带有next()和return(..)方法的迭代器对象。它的状态经过变量n1和n2维护在闭包中。
接下来让咱们考虑一个迭代器,它被设计为执行一系列(也叫队列)动做,一次一个:
在tasks上的迭代器步过在数组属性actions中找到的函数,并每次执行它们中的一个,并传入你传递给next(..)的任何参数值,并在标准的IteratorResult对象中向你返回任何它返回的东西。
这是咱们如何使用这个tasks队列:
这种特别的用法证明了迭代器能够是一种具备组织功能的模式,不只仅是数据。这也联系着咱们在下一节关于generator将要看到的东西。
你甚至能够更有创意一些,在一块数据上定义一个表示元操做的迭代器。例如,咱们能够为默认从0开始递增至(或递减至,对于负数来讲)指定数字的一组数字定义一个迭代器。
考虑以下代码:
如今,这种创意给了咱们什么技巧?
这是一些有趣的技巧,虽然其实际用途有些值得商榷。可是再一次,有人可能想知道为何ES6没有提供如此微小但讨喜的特性呢?
若是我连这样的提醒都没给过你,那就是个人疏忽:像我在前面的代码段中作的那样扩展原生原型,是一件你须要当心并了解潜在的危害后才应该作的事情。
在这样的状况下,你与其余代码或者将来的JS特性发生冲突的可能性很是低。可是要当心微小的可能性。并在文档中为后人详细记录下你在作什么。
消费迭代器
咱们已经看到了使用for..of循环来一个元素一个元素地消费一个迭代器。可是还有一些其余的ES6结构能够消费迭代器。
让咱们考虑一下附着这个数组上的迭代器(虽然任何咱们选择的迭代器都将拥有以下的行为):
扩散操做符...将彻底耗尽一个迭代器。考虑以下代码:
...还能够在一个数组内部扩散一个迭代器:
数组解构(参见第二章的“解构”)能够部分地或者彻底地(若是与一个...剩余/收集操做符一块儿使用)消费一个迭代器:
Generator
全部的函数都会运行至完成,对吧?换句话说,一旦一个函数开始运行,在它完成以前没有任何东西可以打断它。
至少对于到目前为止的JavaScript的整个历史来讲是这样的。在ES6中,引入了一个有些异乎寻常的新形式的函数,称为generator。一个generator能够在运行期间暂停它本身,还能够当即或者稍后继续运行。因此显然它没有普通函数那样的运行至完成的保证。
另外,在运行期间的每次暂停/继续轮回都是一个双向消息传递的好机会,generator能够在这里返回一个值,而使它继续的控制端代码能够发回一个值。
就像前一节中的迭代器同样,有种方式能够考虑generator是什么,或者说它对什么最有用。对此没有一个正确的答案,但咱们将试着从几个角度考虑。
语法
generator函数使用这种新语法声明:
*的位置在功能上可有可无。一样的声明还能够写作如下的任意一种:
这里 惟一 的区别就是风格的偏好。大多数其余的文献彷佛喜欢function* foo(..) { .. }。我喜欢function *foo(..) { .. },因此这就是我将在本书剩余部分中表示它们的方法。
我这样作的理由实质上纯粹是为了教学。在这本书中,当我引用一个generator函数时,我将使用*foo(..),与普通函数的foo(..)相对。我发现*foo(..)与function *foo(..) { .. }中*的位置更加吻合。
另外,就像咱们在第二章的简约方法中看到的,在对象字面量中有一种简约generator形式:
我要说在简约generator中,*foo() { .. }要比* foo() { .. }更天然。这进一步代表了为什么使用*foo()匹配一致性。
一致性使理解与学习更轻松。
执行一个Generator
虽然一个generator使用*进行声明,可是你依然能够像一个普通函数那样执行它:
你依然能够传给它参数值,就像:
主要区别在于,执行一个generator,好比foo(5,10),并不实际运行generator中的代码。取而代之的是,它生成一个迭代器来控制generator执行它的代码。
咱们将在稍后的“迭代器控制”中回到这个话题,可是简要地说:
yield
Generator还有一个你能够在它们内部使用的新关键字,用来表示暂停点:yield。考虑以下代码:
在这个*foo()generator中,前两行的操做将会在开始时运行,而后yield将会暂停这个generator。若是这个generator被继续,*foo()的最后一行将运行。在一个generator中yield能够出现任意屡次(或者,在技术上讲,根本不出现!)。
你甚至能够在一个循环内部放置yield,它能够表示一个重复的暂停点。事实上,一个永不完成的循环就意味着一个永不完成的generator,这是彻底合法的,并且有时候彻底是你须要的。
yield不仅是一个暂停点。它是在暂停generator时发送出一个值的表达式。这里是一个位于generator中的while..true循环,它每次迭代时yield出一个新的随机数:
yield ..表达式不只发送一个值 —— 不带值的yield与yield undefined相同 —— 它还接收(也就是,被替换为)最终的继续值。考虑以下代码:
这个generator在暂停它本身时将首先yield出值10。当你继续这个generator时 —— 使用咱们先前提到的it.next(..) —— 不管你使用什么值继续它,这个值都将替换/完成整个表达式yield 10,这意味着这个值将被赋值给变量x
一个yield..表达式能够出如今任意普通表达式可能出现的地方。例如:
这里的*foo()有四个yield ..表达式。其中每一个yield都会致使generator暂停以等待一个继续值,这个继续值稍后被用于各个表达式环境中。
yield在技术上讲不是一个操做符,虽然像yield 1这样使用时看起来确实很像。由于yield能够像var x = yield这样彻底经过本身被使用,因此将它认为是一个操做符有时使人困惑。
从技术上讲,yield ..与a = 3这样的赋值表达式拥有相同的“表达式优先级” —— 概念上和操做符优先级很类似。这意味着yield ..基本上能够出如今任何a = 3能够合法出现的地方。
让咱们展现一下这种对称性:
注意: 若是你好好考虑一下,认为一个yield ..表达式与一个赋值表达式的行为类似在概念上有些道理。当一个被暂停的generator被继续时,它就以一种与被这个继续值“赋值”区别不大的方式,被这个值完成/替换。
要点:若是你须要yield ..出如今a = 3这样的赋值本不被容许出现的位置,那么它就须要被包在一个( )中。
由于yield关键字的优先级很低,几乎任何出如今yield ..以后的表达式都会在被yield发送以前首先被计算。只有扩散操做符...和逗号操做符,拥有更低的优先级,这意味着他们会在yield已经被求值以后才会被处理。
因此正如带有多个操做符的普通语句同样,存在另外一个可能须要( )来覆盖(提高)yield的低优先级的状况,就像这些表达式之间的区别:
和=赋值同样,yield也是“右结合性”的,这意味着多个接连出现的yield表达式被视为从右到左被( .. )分组。因此,yield yield yield 3将被视为yield (yield (yield 3))。像((yield) yield) yield 3这样的“左结合性”解释没有意义。
和其余操做符同样,yield与其余操做符或yield组合时为了使你的意图没有歧义,使用( .. )分组是一个好主意,即便这不是严格要求的。
注意: 更多关于操做符优先级和结合性的信息,参见本系列的 类型与文法。
yield *
与*使一个function声明成为一个function *generator声明的方式同样,一个*使yield成为一个机制很是不一样的yield *,称为 yield委托。从文法上讲,yield *..的行为与yield ..相同,就像在前一节讨论过的那样。
yield * ..须要一个可迭代对象;而后它调用这个可迭代对象的迭代器,并将它本身的宿主generator的控制权委托给那个迭代器,直到它被耗尽。考虑以下代码:
注意: 与generator声明中*的位置(早先讨论过)同样,在yield *表达式中的*的位置在风格上由你来决定。大多数其余文献偏好yield* ..,可是我喜欢yield *..,理由和咱们已经讨论过的相同。
值[1,2,3]产生一个将会步过它的值的迭代器,因此generator*foo()将会在被消费时产生这些值。另外一种说明这种行为的方式是,yield委托到了另外一个generator:
当*bar()调用*foo()产生的迭代器经过yield *受到委托,意味着不管*foo()产生什么值都会被*bar()产生。
在yield ..中表达式的完成值来自于使用it.next(..)继续generator,而yield *..表达式的完成值来自于受到委托的迭代器的返回值(若是有的话)。
内建的迭代器通常没有返回值,正如咱们在本章早先的“迭代器循环”一节的末尾讲过的。可是若是你定义你本身的迭代器(或者generator),你就能够将它设计为return一个值,yield *..将会捕获它:
虽然值1,2,和3从*foo()中被yield出来,而后从*bar()中被yield出来,可是从*foo()中返回的值4是表达式yield *foo()的完成值,而后它被赋值给x。
由于yield *能够调用另外一个generator(经过委托到它的迭代器的方式),它还能够经过调用本身来实施某种generator递归:
取得foo(1)的结果并调用迭代器的next()来使它运行它的递归步骤,结果将是24。第一次*foo()运行时x拥有值1,它是x < 3。x + 1被递归地传递到*foo(..),因此以后的x是2。再一次递归调用致使x为3。
如今,由于x < 3失败了,递归中止,并且return 3 * 2将6给回前一个调用的yeild *..表达式,它被赋值给x。另外一个return 6 * 2返回12给前一个调用的x。最终12 * 2,即24,从generator*foo(..)运行的完成中被返回。
迭代器控制
早先,咱们简要地介绍了generator是由迭代器控制的概念。如今让咱们完整地深刻这个话题。
回忆一下前一节的递归*for(..)。这是咱们如何运行它:
在这种状况下,generator并无真正暂停过,由于这里没有yield ..表达式。而yield *只是经过递归调用保持当前的迭代步骤继续运行下去。因此,仅仅对迭代器的next()函数进行一次调用就彻底地运行了generator。
如今让咱们考虑一个有多个步骤而且所以有多个产生值的generator:
咱们已经知道咱们能够是使用一个for..of循环来消费一个迭代器,即使它是一个附着在*foo()这样的generator上:
注意: for..of循环须要一个可迭代对象。一个generator函数引用(好比foo)自己不是一个可迭代对象;你必须使用foo()来执行它以获得迭代器(它也是一个可迭代对象,正如咱们在本章早先讲解过的)。理论上你可使用一个实质上仅仅执行return this()的Symbol.iterator函数来扩展GeneratorPrototype(全部generator函数的原型)。这将使foo引用自己成为一个可迭代对象,也就意味着for (var v of foo) { .. }(注意在foo上没有())将能够工做。
让咱们手动迭代这个generator:
若是你仔细观察,这里有三个yield语句和四个next()调用。这可能看起来像是一个奇怪的不匹配。事实上,假定全部的东西都被求值而且generator彻底运行至完成的话,next()调用将老是比yield表达式多一个。
可是若是你相反的角度观察(从里向外而不是从外向里),yield和next()之间的匹配就显得更有道理。
回忆一下,yield ..表达式将被你用于继续generator的值完成。这意味着你传递给next(..)的参数值将完成任何当前暂停中等待完成的yield ..表达式。
让咱们这样展现一下这种视角:
在这个代码段中,每一个yield ..都送出一个值(1,2,3),但更直接的是,它暂停了generator来等待一个值。换句话说,它就像在问这样一个问题,“我应当在这里用什么值?我会在这里等你告诉我。”
如今,这是咱们如何控制*foo()来启动它:
这第一个next()调用从generator初始的暂停状态启动了它,并运行至第一个yield。在你调用第一个next()的那一刻,并无yield ..表达式等待完成。若是你给第一个next()调用传递一个值,目前它会被扔掉,由于没有yield等着接受这样的一个值。
注意: 一个“ES6以后”时间表中的早期提案 将 容许你在generator内部经过一个分离的元属性(见第七章)来访问一个被传入初始next(..)调用的值。
如今,让咱们回答那个未解的问题,“我应当给x赋什么值?” 咱们将经过给 下一个 next(..)调用发送一个值来回答:
如今,x将拥有值"foo",但咱们也问了一个新的问题,“我应当给y赋什么值?”
答案给出了,另外一个问题被提出了。最终答案:
如今,每个yield ..的“问题”是如何被 下一个 next(..)调用回答的,因此咱们观察到的那个“额外的”next()调用老是使一切开始的那一个。
让咱们把这些步骤放在一块儿:
在生成器的每次迭代都简单地为消费者生成一个值的状况下,你可认为一个generator是一个值的生成器。
可是在更通常的意义上,也许将generator认为是一个受控制的,累进的代码执行过程更恰当,与早先“自定义迭代器”一节中的tasks队列的例子很是相像。
注意: 这种视角正是咱们将如何在第四章中重温generator的动力。特别是,next(..)没有理由必定要在前一个next(..)完成以后当即被调用。虽然generator的内部执行环境被暂停了,程序的其余部分仍然没有被阻塞,这包括控制generator何时被继续的异步动做能力。
提早完成
正如咱们在本章早先讲过的,链接到一个generator的迭代器支持可选的return(..)和throw(..)方法。它们俩都有当即停止一个暂停的的generator的效果。
考虑以下代码:
return(x)有点像强制一个return x就在那个时刻被处理,这样你就当即获得这个指定的值。一旦一个generator完成,不管是正常地仍是像展现的那样提早地,它就再也不处理任何代码或返回任何值了。
return(..)除了能够手动调用,它还在迭代的最后被任何ES6中消费迭代器的结构自动调用,好比for..of循环和...扩散操做符。
这种能力的目的是,在控制端的代码再也不继续迭代generator时它能够收到通知,这样它就可能作一些清理工做(释放资源,复位状态,等等)。与普通函数的清理模式彻底相同,达成这个目的的主要方法是使用一个finally子句:
警告: 不要把yield语句放在finally子句内部!它是有效和合法的,但这确实是一个可怕的主意。它在某种意义上推迟了return(..)调用的完成,由于在finally子句中的任何yield ..表达式都被遵循来暂停和发送消息;你不会像指望的那样当即获得一个完成的generator。基本上没有任何好的理由去选择这种疯狂的 坏的部分,因此避免这么作!
前一个代码段除了展现return(..)如何在停止generator的同时触发finally子句,它还展现了一个generator在每次被调用时都产生一个全新的迭代器。事实上,你能够并发地使用链接到相同generator的多个迭代器:
提早停止
你能够调用throw(..)来代替return(..)调用。就像return(x)实质上在generator当前的暂停点上注入了一个return x同样,调用throw(x)实质上就像在暂停点上注入了一个throw x。
除了处理异常的行为(咱们在下一节讲解这对try子句意味着什么),throw(..)产生相同的提早完成 —— 在generator当前的暂停点停止它的运行。例如:
由于throw(..)基本上注入了一个throw ..来替换generator的yield 1这一行,并且没有东西处理这个异常,它当即传播回外面的调用端代码,调用端代码使用了一个try..catch来处理了它。
与return(..)不一样的是,迭代器的throw(..)方法毫不会被自动调用。
固然,虽然没有在前面的代码段中展现,但若是当你调用throw(..)时有一个try..finally子句等在generator内部的话,这个finally子句将会在异常被传播回调用端代码以前有机会运行。
错误处理
正如咱们已经获得的提示,generator中的错误处理可使用try..catch表达,它在上行和下行两个方向均可以工做。
错误也能够经过yield *委托在两个方向上传播:
当*foo()调用yield 1时,值1原封不动地穿过了*bar(),就像咱们已经看到过的那样。
但这个代码段最有趣的部分是,当*foo()调用throw "foo: e2"时,这个错误传播到了*bar()并当即被*bar()的try..catch块儿捕获。错误没有像值1那样穿过*bar()。
而后*bar()的catch将err普通地输出("foo: e2")以后*bar()就正常结束了,这就是为何迭代器结果{ value: undefined, done: true }从it.next()中返回。
若是*bar()没有用try..catch环绕着yield *..表达式,那么错误将理所固然地一直传播出来,并且在它传播的路径上依然会完成(停止)*bar()。
转译一个Generator
有可能在ES6以前的环境中表达generator的能力吗?事实上是能够的,并且有好几种了不得的工具在这么作,包括最著名的Facebook的Regenerator工具 (https://facebook.github.io/regenerator/)。
但为了更好地理解generator,让咱们试着手动转换一下。基本上讲,咱们将制造一个简单的基于闭包的状态机。
咱们将使本来的generator很是简单:
开始以前,咱们将须要一个咱们可以执行的称为foo()的函数,它须要返回一个迭代器:
如今,咱们须要一些内部变量来持续跟踪咱们的“generator”的逻辑走到了哪个步骤。咱们称它为state。咱们将有三种状态:起始状态的0,等待完成yield表达式的1,和generator完成的2。
每次next(..)被调用时,咱们须要处理下一个步骤,而后递增state。为了方便,咱们将每一个步骤放在一个switch语句的case子句中,而且咱们将它放在一个next(..)能够调用的称为nextState(..)的内部函数中。另外,由于x是一个横跨整个“generator”做用域的变量,因此它须要存活在nextState(..)函数的外部。
这是将它们放在一块儿(很明显,为了使概念的展现更清晰,它通过了某些简化):
最后,让咱们测试一下咱们的前ES6“generator”:
不赖吧?但愿这个练习能在你的脑中巩固这个概念:generator实际上只是状态机逻辑的简单语法。这使它们能够普遍地应用。
Generator的使用
咱们如今很是深刻地理解了generator如何工做,那么,它们在什么地方有用?
咱们已经看过了两种主要模式:
生产一系列值: 这种用法能够很简单(例如,随机字符串或者递增的数字),或者它也能够表达更加结构化的数据访问(例如,迭代一个数据库查询结果的全部行)。
这两种方式中,咱们使用迭代器来控制generator,这样就能够为每次next(..)调用执行一些逻辑。在数据解构上的普通迭代器只不过生成值而没有任何控制逻辑。
串行执行的任务队列: 这种用法常常用来表达一个算法中步骤的流程控制,其中每一步都要求从某些外部数据源取得数据。对每块儿数据的请求可能会当即知足,或者可能会异步延迟地知足。
从generator内部代码的角度来看,在yield的地方,同步或异步的细节是彻底不透明的。另外,这些细节被有意地抽象出去,如此就不会让这样的实现细节把各个步骤间天然的,顺序的表达搞得模糊不清。抽象还意味着实现能够被替换/重构,而根本不用碰generator中的代码。
当根据这些用法观察generator时,它们的含义要比仅仅是手动状态机的一种不一样或更好的语法多多了。它们是一种用于组织和控制有序地生产与消费数据的强大工具。