详解JavaScript ES6中的Generator
今天讨论的新特性让我非常兴奋,因为这个特性是ES6中最神奇的特性。
这里的“神奇”意味着什么呢?对于初学者来说,该特性与以往的JS完全不同,甚至有些晦涩难懂。从某种意义上说,它完全改变了这门语言的通常行为,这不是“神奇”是什么呢。
不仅如此,该特性还可以简化程序代码,将复杂的“回调堆栈”改成直线执行的形式。
我是不是铺垫的太多了?下面开始深入介绍,你自己去判断吧。
简介
什么是Generator?
看下面代码:
function*quips(name){ yield"hello"+name+"!"; yield"ihopeyouareenjoyingtheblogposts"; if(name.startsWith("X")){ yield"it'scoolhowyournamestartswithX,"+name; } yield"seeyoulater!"; } function*quips(name){ yield"hello"+name+"!"; yield"ihopeyouareenjoyingtheblogposts"; if(name.startsWith("X")){ yield"it'scoolhowyournamestartswithX,"+name; } yield"seeyoulater!"; }
上面代码是模仿Talkingcat(当下一个非常流行的应用)的一部分,点击这里试玩,如果你对代码感到困惑,那就回到这里来看下面的解释。
这看上去很像一个函数,这被称为Generator函数,它与我们常见的函数有很多共同点,但还可以看到下面两个差异:
通常的函数以function开始,但Generator函数以function*开始。
在Generator函数内部,yield是一个关键字,和return有点像。不同点在于,所有函数(包括Generator函数)都只能返回一次,而在Generator函数中可以yield任意次。yield表达式暂停了Generator函数的执行,然后可以从暂停的地方恢复执行。
常见的函数不能暂停执行,而Generator函数可以,这就是这两者最大的区别。
原理
调用quips()时发生了什么?
>variter=quips("jorendorff"); [objectGenerator] >iter.next() {value:"hellojorendorff!",done:false} >iter.next() {value:"ihopeyouareenjoyingtheblogposts",done:false} >iter.next() {value:"seeyoulater!",done:false} >iter.next() {value:undefined,done:true} >variter=quips("jorendorff"); [objectGenerator] >iter.next() {value:"hellojorendorff!",done:false} >iter.next() {value:"ihopeyouareenjoyingtheblogposts",done:false} >iter.next() {value:"seeyoulater!",done:false} >iter.next() {value:undefined,done:true}
我们对普通函数的行为非常熟悉,函数被调用时就立即执行,直到函数返回或抛出一个异常,这是所有JS程序员的第二天性。
Generator函数的调用方法与普通函数一样:quips("jorendorff"),但调用一个Generator函数时并没有立即执行,而是返回了一个Generator对象(上面代码中的iter),这时函数就立即暂停在函数代码的第一行。
每次调用Generator对象的.next()方法时,函数就开始执行,直到遇到下一个yield表达式为止。
这就是为什么我们每次调用iter.next()时都会得到一个不同的字符串,这些都是在函数内部通过yield表达式产生的值。
当执行最后一个iter.next()时,就到达了Generator函数的末尾,所以返回结果的.done属性值为true,并且.value属性值为undefined。
现在,回到Talkingcat的DEMO,尝试在代码中添加一些yield表达式,看看会发生什么。
从技术层面上讲,每当Generator函数执行遇到yield表达式时,函数的栈帧—本地变量,函数参数,临时值和当前执行的位置,就从堆栈移除,但是Generator对象保留了对该栈帧的引用,所以下次调用.next()方法时,就可以恢复并继续执行。
值得提醒的是Generator并不是多线程。在支持多线程的语言中,同一时间可以执行多段代码,并伴随着执行资源的竞争,执行结果的不确定性和较好的性能。而Generator函数并不是这样,当一个Generator函数执行时,它与其调用者都在同一线程中执行,每次执行顺序都是确定的,有序的,并且执行顺序不会发生改变。与线程不同,Generator函数可以在内部的yield的标志点暂停执行。
通过介绍Generator函数的暂停、执行和恢复执行,我们知道了什么是Generator函数,那么现在抛出一个问题:Generator函数到底有什么用呢?
迭代器
通过上篇文章,我们知道迭代器并不是ES6的一个内置的类,而只是作为语言的一个扩展点,你可以通过实现[Symbol.iterator]()和.next()方法来定义一个迭代器。
但是,实现一个接口还是需要写一些代码的,下面我们来看看在实际中如何实现一个迭代器,以实现一个range迭代器为例,该迭代器只是简单地从一个数累加到另一个数,有点像C语言中的for(;;)循环。
//Thisshould"ding"threetimes for(varvalueofrange(0,3)){ alert("Ding!atfloor#"+value); } //Thisshould"ding"threetimes for(varvalueofrange(0,3)){ alert("Ding!atfloor#"+value); }
现在有一个解决方案,就是使用ES6的类。(如果你对class语法还不熟悉,不要紧,我会在将来的文章中介绍。)
classRangeIterator{ constructor(start,stop){ this.value=start; this.stop=stop; } [Symbol.iterator](){returnthis;} next(){ varvalue=this.value; if(value<this.stop){ this.value++; return{done:false,value:value}; }else{ return{done:true,value:undefined}; } } } //Returnanewiteratorthatcountsupfrom'start'to'stop'. functionrange(start,stop){ returnnewRangeIterator(start,stop); } classRangeIterator{ constructor(start,stop){ this.value=start; this.stop=stop; } [Symbol.iterator](){returnthis;} next(){ varvalue=this.value; if(value<this.stop){ this.value++; return{done:false,value:value}; }else{ return{done:true,value:undefined}; } } } //Returnanewiteratorthatcountsupfrom'start'to'stop'. functionrange(start,stop){ returnnewRangeIterator(start,stop); }
查看该DEMO。
这种实现方式与Java和Swift的实现方式类似,看上去还不错,但还不能说上面代码就完全正确,代码没有任何Bug?这很难说。我们看不到任何传统的for(;;)循环代码:迭代器的协议迫使我们将循环拆散了。
在这一点上,你也许会对迭代器不那么热衷了,它们使用起来很方便,但是实现起来似乎很难。
我们可以引入一种新的实现方式,以使得实现迭代器更加容易。上面介绍的Generator可以用在这里吗?我们来试试:
function*range(start,stop){ for(vari=start;i<stop;i++) yieldi; } function*range(start,stop){ for(vari=start;i<stop;i++) yieldi; }
查看该DEMO。
上面这4行代码就可以完全替代之前的那个23行的实现,替换掉整个RangeIterator类,这是因为Generator天生就是迭代器,所有的Generator都原生实现了.next()和[Symbol.iterator]()方法。你只需要实现其中的循环逻辑就够了。
不使用Generator去实现一个迭代器就像被迫写一个很长很长的邮件一样,本来简单的表达出你的意思就可以了,RangeIterator的实现是冗长和令人费解的,因为它没有使用循环语法去实现一个循环功能。使用Generator才是我们需要掌握的实现方式。
我们可以使用作为迭代器的Generator的哪些功能呢?
使任何对象可遍历—编写一个Genetator函数去遍历this,每遍历到一个值就yield一下,然后将该Generator函数作为要遍历的对象上的[Symbol.iterator]方法的实现。
简化返回数组的函数—假如有一个每次调用时都返回一个数组的函数,比如:
//Dividetheone-dimensionalarray'icons' //intoarraysoflength'rowLength'. functionsplitIntoRows(icons,rowLength){ varrows=[]; for(vari=0;i<icons.length;i+=rowLength){ rows.push(icons.slice(i,i+rowLength)); } returnrows; } //Dividetheone-dimensionalarray'icons' //intoarraysoflength'rowLength'. functionsplitIntoRows(icons,rowLength){ varrows=[]; for(vari=0;i<icons.length;i+=rowLength){ rows.push(icons.slice(i,i+rowLength)); } returnrows; }
使用Generator可以简化这类函数:
function*splitIntoRows(icons,rowLength){ for(vari=0;i<icons.length;i+=rowLength){ yieldicons.slice(i,i+rowLength); } } function*splitIntoRows(icons,rowLength){ for(vari=0;i<icons.length;i+=rowLength){ yieldicons.slice(i,i+rowLength); } }
这两者唯一的区别在于,前者在调用时计算出了所有结果并用一个数组返回,后者返回的是一个迭代器,结果是在需要的时候才进行计算,然后一个一个地返回。
无穷大的结果集—我们不能构建一个无穷大的数组,但是我们可以返回一个生成无尽序列的Generator,并且每个调用者都可以从中获取到任意多个需要的值。
重构复杂的循环—你是否想将一个复杂冗长的函数重构为两个简单的函数?Generator是你重构工具箱中一把新的瑞士军刀。对于一个复杂的循环,我们可以将生成数据集那部分代码重构为一个Generator函数,然后用for-of遍历:for(vardataofmyNewGenerator(args))。
构建迭代器的工具—ES6并没有提供一个可扩展的库,来对数据集进行filter和map等操作,但Generator可以用几行代码就实现这类功能。
例如,假设你需要在Nodelist上实现与Array.prototype.filter同样的功能的方法。小菜一碟的事:
function*filter(test,iterable){ for(varitemofiterable){ if(test(item)) yielditem; } } function*filter(test,iterable){ for(varitemofiterable){ if(test(item)) yielditem; } }
所以,Generator很实用吧?当然,这是实现自定义迭代器最简单直接的方式,并且,在ES6中,迭代器是数据集和循环的新标准。
但,这还不是Generator的全部功能。
异步代码
异步API通常都需要一个回调函数,这意味着每次你都需要编写一个匿名函数来处理异步结果。如果同时处理三个异步事务,我们看到的是三个缩进层次的代码,而不仅仅是三行代码。
看下面代码:
}).on('close',function(){ done(undefined,undefined); }).on('error',function(error){ done(error); }); }).on('close',function(){ done(undefined,undefined); }).on('error',function(error){ done(error); });
异步API通常都有错误处理的约定,不同的API有不同的约定。大多数情况下,错误是默认丢弃的,甚至有些将成功也默认丢弃了。
直到现在,这些问题仍是我们处理异步编程必须付出的代价,而且我们也已经接受了异步代码只是看不来不像同步代码那样简单和友好。
Generator给我们带来了希望,我们可以不再采用上面的方式。
Q.async()是一个将Generator和Promise结合起来处理异步代码的实验性尝试,让我们的异步代码类似于相应的同步代码。
例如:
//Synchronouscodetomakesomenoise. functionmakeNoise(){ shake(); rattle(); roll(); } //Asynchronouscodetomakesomenoise. //ReturnsaPromiseobjectthatbecomesresolved //whenwe'redonemakingnoise. functionmakeNoise_async(){ returnQ.async(function*(){ yieldshake_async(); yieldrattle_async(); yieldroll_async(); }); } //Synchronouscodetomakesomenoise. functionmakeNoise(){ shake(); rattle(); roll(); } //Asynchronouscodetomakesomenoise. //ReturnsaPromiseobjectthatbecomesresolved //whenwe'redonemakingnoise. functionmakeNoise_async(){ returnQ.async(function*(){ yieldshake_async(); yieldrattle_async(); yieldroll_async(); }); }
最大的区别在于,需要在每个异步方法调用的前面添加yield关键字。
在Q.async中,添加一个if语句或try-catch异常处理,就和在同步代码中的方式一样,与其他编写异步代码的方式相比,减少了很多学习成本。
Generator为我们提供了一种更适合人脑思维方式的异步编程模型。但更好的语法也许更有帮助,在ES7中,一个基于Promise和Generator的异步处理函数正在规划之中,灵感来自C#中类似的特性。
兼容性
在服务器端,现在就可以直接在io.js中使用Generator(或者在NodeJs中以--harmony启动参数来启动Node)。
在浏览器端,目前只有Firefox27和Chrome39以上的版本才支持Generator,如果想直接在Web上使用,你可以使用Babel或Google的Traceur将ES6代码转换为Web友好的ES5代码。
一些题外话:JS版本的Generator最早是由BrendanEich实现,他借鉴了PythonGenerator的实现,该实现的灵感来自Icon,早在2006年的Firefox2.0就吸纳了Generator。但标准化的道路是坎坷的,一路下来,其语法和行为都发生了很多改变,Firefox和Chrome中的ES6Generator是由AndyWingo实现,这项工作是由Bloomberg赞助的。
yield;
关于Generator还有一些未提及的部分,我们还没有涉及到.throw()和.return()方法的使用,.next()方法的可选参数,还有yield*语法。但我认为这篇文章已经够长了,就像Generator一样,我们也暂停一下,另外找个时间再剩余的部分。
我们已经介绍了ES6中两个非常重要的特性,那么现在可以大胆地说,ES6将改变我们的生活,看似简单的特性,却有极大的用处。