我們將要討論 ES6 的最神奇的功能。
我所說(shuō)的“神奇”的意思是什么呢?對(duì)于初學(xué)者來(lái)說(shuō),該功能與 JS 中已經(jīng)存在的功能非常不同。這可能會(huì)讓初學(xué)者們?cè)诘谝淮我?jiàn)到這個(gè)功能的時(shí)候感覺(jué)晦澀難懂。從某種意義上來(lái)講,這個(gè)功能使得語(yǔ)言的內(nèi)在的很平常的行為展露在開(kāi)發(fā)者面前。如果這還不算是神奇,那我不知道什么才算了。
不僅如此:這個(gè)功能能夠簡(jiǎn)化代碼并且使得在超類上的“回調(diào)地獄”更加直白。
是不是我太看重這個(gè)了?讓我們一起深入,然后你自己就可以進(jìn)行判斷了。
什么是生成器?
我們先從下面的這里例子開(kāi)始。
function* quips(name) {
yield "hello " + name + "!";
yield "i hope you are enjoying the blog posts";
if (name.startsWith("X")) {
yield "it's cool how your name starts with X, " + name;
}
yield "see you later!";
}
這段代碼是一個(gè)對(duì)話貓,這可能是當(dāng)前網(wǎng)絡(luò)上最重要的一類應(yīng)用。( 繼續(xù),點(diǎn)擊鏈接,與貓一起玩耍。當(dāng)你徹底困惑了以后,你再回到這里看一下解釋。)
這個(gè)在一定程度上看起來(lái)像一個(gè)函數(shù),對(duì)不?這就被稱為生成器函數(shù),同時(shí)它與函數(shù)之間也有很多相似之處。但是你一下子就能發(fā)現(xiàn)兩個(gè)不同之處:
就是這樣的,以上就是普通的函數(shù)和生成器函數(shù)之間的大區(qū)別。普通的函數(shù)不能自己暫停。然而生成器函數(shù)可以自己暫停運(yùn)行。
當(dāng)你調(diào)用 quips( ) 生成器函數(shù)時(shí)將會(huì)發(fā)生什么?
> var iter = quips("jorendorff");
[object Generator]
> iter.next()
{ value: "hello jorendorff!", done: false }
> iter.next()
{ value: "i hope you are enjoying the blog posts", done: false }
> iter.next()
{ value: "see you later!", done: false }
> iter.next()
{ value: undefined, done: true }
你可能非常習(xí)慣于普通的函數(shù)以及他們的表現(xiàn)。當(dāng)你調(diào)用他們的時(shí)候,他們立即開(kāi)始運(yùn)行,當(dāng)遇到 return 或者 throw 的時(shí)候,他們停止運(yùn)行。任何一個(gè) JS 程序員都非常習(xí)慣于上述的過(guò)程。
調(diào)用一個(gè)生成器看起來(lái)是一樣的:quips("jorendorff")。 但是當(dāng)你調(diào)用一個(gè)生成器,他還不開(kāi)始運(yùn)行。反而,它返回一個(gè)暫停的生成器對(duì)象( 在上述的例子中被稱為 iter )。你可以認(rèn)為這個(gè)生成器對(duì)象是一個(gè)函數(shù)調(diào)用,暫時(shí)停止。特別的是,其在生成器函數(shù)一開(kāi)始就停止了,在運(yùn)行代碼的第一行之前。
每次你調(diào)用生成器對(duì)象的 .next( ) 方法,函數(shù)將其自己解凍并運(yùn)行直到其到達(dá)下一個(gè) yield 表達(dá)式。
這就是上面代碼中我們?yōu)槭裁匆{(diào)用 iter.next( ),調(diào)用后我們獲得一個(gè)不同的字符串值。這些值都是由 quips( ) 里的 yield 表達(dá)式產(chǎn)生的。
在最后一個(gè) iter.next( ) 調(diào)用中,我們最后結(jié)束了生成器函數(shù),所以結(jié)果的 .done 領(lǐng)域的值為true。 一個(gè)函數(shù)的結(jié)束就像是返回 undefined,而且這也是為什么結(jié)果的 .value 領(lǐng)域是不確定的 ( undefined)。
現(xiàn)在可能是一個(gè)好機(jī)會(huì)來(lái)返回到上面的對(duì)話貓的例子那頁(yè)面上,同時(shí)真正地可以玩轉(zhuǎn)代碼。嘗試著在一個(gè)循環(huán)中加入一個(gè) yield。這會(huì)發(fā)生什么呢?
在技術(shù)層面上,每一次一個(gè)生成器進(jìn)行 yield 操作,其堆棧楨,包括局部變量,參數(shù),臨時(shí)值,以及在生成器中的執(zhí)行的當(dāng)前位置,被從棧中刪除。然而,生成器對(duì)象保有這個(gè)堆棧幀的引用(或者是副本)。因此,接下來(lái)的調(diào)用 .next( ) 可以重新激活它并繼續(xù)執(zhí)行。
值得指出的是,生成器都沒(méi)有線程。在能使用線程的語(yǔ)言中,多份代碼可以在同一時(shí)間運(yùn)行,這通常導(dǎo)致了競(jìng)爭(zhēng)條件,非確定性和非常非常好的性能。生成器和這完全不同。當(dāng)生成器運(yùn)行時(shí),它與調(diào)用者運(yùn)行在同一個(gè)線程中。執(zhí)行的順序是連續(xù)且確定的,并永遠(yuǎn)不會(huì)并發(fā)。不同于系統(tǒng)線程,生成器只會(huì)在代碼中用 yield 標(biāo)記的地方才會(huì)懸掛。
好了。我們知道生成器是什么了。我們已經(jīng)看到了生成器器運(yùn)行,暫停,然后恢復(fù)執(zhí)行。現(xiàn)在的大問(wèn)題是,這樣奇怪的能力怎么可能是有用的呢?
上周,我們已經(jīng)看到了 ES6 的迭代器不只是一個(gè)簡(jiǎn)單的內(nèi)置類。他們是語(yǔ)言的擴(kuò)展點(diǎn)。你可以通過(guò)實(shí)現(xiàn)兩種方法來(lái)創(chuàng)建你自己的迭代器,這兩種方法是:[ Symbol.iterator ]( )和 .next( )。
但是,實(shí)現(xiàn)接口總是至少還是有一點(diǎn)工作量的。讓我們來(lái)看看一個(gè)迭代器實(shí)現(xiàn)在實(shí)踐中看起來(lái)是什么樣的。因?yàn)槭且粋€(gè)例子,讓我們使用一個(gè)簡(jiǎn)單的范圍 ( range ) 迭代器,只簡(jiǎn)單的從一個(gè)數(shù)字?jǐn)?shù)到另一個(gè)數(shù)字,就像一個(gè)老式的 C 語(yǔ)言的 for ( ; ; ) 循環(huán)。
// 這個(gè)應(yīng)該三次發(fā)出“叮”的聲音
for (var value of range(0, 3)) {
alert("Ding! at floor #" + value);
}
這里是一個(gè)使用 ES6 的類的解決方案。( 如果類的語(yǔ)言沒(méi)有完全的清楚,不要擔(dān)心——我們將在未來(lái)的博客中解決這個(gè)問(wèn)題。)
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
} else {
return {done: true, value: undefined};
}
}
}
// 返回一個(gè)新的從“開(kāi)始”數(shù)到“結(jié)束”的迭代器。
function range(start, stop) {
return new RangeIterator(start, stop);
}
在實(shí)際運(yùn)行中看這段代碼 ( http://codepen.io/anon/pen/NqGgOQ )。
這就是像是在 Java 或 Swift 語(yǔ)言里實(shí)現(xiàn)一個(gè)迭代器。它不是那么糟糕。但它也并不是那么簡(jiǎn)單。在這個(gè)代碼中有沒(méi)有任何錯(cuò)誤?這就不好說(shuō)了。它看起來(lái)完全不像我們想在這里模仿的原來(lái)的 for ( ; ; ) 循環(huán):迭代器協(xié)議迫使我們拋棄了循環(huán)。
在這一點(diǎn)上,你可能會(huì)對(duì)迭代器不太熱情。他們可能對(duì)使用來(lái)說(shuō)很棒,但他們似乎很難實(shí)現(xiàn)。
你可能不會(huì)建議我們只是為了簡(jiǎn)單的創(chuàng)建迭代器,而在 JS 語(yǔ)言中引進(jìn)一個(gè)復(fù)雜的新的控制流結(jié)構(gòu)。但是,因?yàn)槲覀兇_實(shí)有生成器,我們能在這里使用它們嗎?讓我們?cè)囈辉嚕?/p>
function* range(start, stop) {
for (var i = start; i < stop; i++)
yield i;
}
在這里看代碼的具體運(yùn)行 ( http://codepen.io/anon/pen/mJewga ) 。
上述的四行的生成器是對(duì)先前 range( ) 的二十三行的實(shí)現(xiàn)的一個(gè)直接替代,包括了整個(gè) RangeIterator 類。這可能是因?yàn)樯善魇堑?。所有的生成器都有一個(gè)內(nèi)置的對(duì) .next() 已經(jīng) [Symbol.iterator]( )方法的實(shí)現(xiàn)。
不使用生成器來(lái)實(shí)現(xiàn)迭代器就像是被強(qiáng)迫用被動(dòng)語(yǔ)氣寫(xiě)一封很長(zhǎng)的郵件。本來(lái)想簡(jiǎn)單地想表達(dá)你的意思,可能到最后你說(shuō)的會(huì)變得相當(dāng)令人費(fèi)解。RangeIterator 是很長(zhǎng)且怪異的,因?yàn)樗仨毑皇褂醚h(huán)語(yǔ)法來(lái)描述一個(gè)循環(huán)的功能。生成器是答案。
我們還能如何使用生成器作為迭代器的能力?
// 將一維數(shù)組 'icons'分為長(zhǎng)度為 'rowLength'的數(shù)組。
function splitIntoRows(icons, rowLength) {
var rows = [];
for (var i = 0; i < icons.length; i += rowLength) {
rows.push(icons.slice(i, i + rowLength));
}
return rows;
}
生成器可以將代碼縮短很多:
function* splitIntoRows(icons, rowLength) {
for (var i = 0; i < icons.length; i += rowLength) {
yield icons.slice(i, i + rowLength);
}
}
在行為上唯一的不同之處在于,取代一次性計(jì)算所有的結(jié)果,并返回他們的一個(gè)數(shù)組,這里返回一個(gè)迭代器且其可以按需一個(gè)一個(gè)地計(jì)算結(jié)果。
特別大量的結(jié)果。你不能構(gòu)建一個(gè)無(wú)窮大的數(shù)組。但是你可以返回一個(gè)生成器,其可以生成一個(gè)無(wú)限大的序列,同時(shí)每一個(gè)調(diào)用者都可以使用它不管他們需要多少個(gè)值。
重構(gòu)復(fù)雜的循環(huán)。你有龐大而丑陋的函數(shù)嗎?你是不是想將它分為兩個(gè)簡(jiǎn)單的部分呢?生成器就是可以幫助你達(dá)成這一目標(biāo)的成套的重構(gòu)工具。當(dāng)你面對(duì)一個(gè)復(fù)雜的循環(huán),你可以將產(chǎn)生數(shù)據(jù)的代碼抽取出來(lái)編程一個(gè)獨(dú)立的生成器函數(shù)。然后改變循環(huán)為 for 循環(huán) ( myNewGenerator(args) 的 var 數(shù)據(jù))。
舉個(gè)例子說(shuō),假設(shè)你需要一個(gè)東西等同于 Array.prototype.filter,其是在 DOM NodeLists 上工作的,不只是一個(gè)數(shù)組。
這用代碼來(lái)實(shí)現(xiàn)就是小意思:
function* filter(test, iterable) {
for (var item of iterable) {
if (test(item))
yield item;
}
}
這個(gè)就是為什么生成器如此有用嗎?當(dāng)然。他們是要實(shí)現(xiàn)自定義的迭代器的簡(jiǎn)單的方法。同時(shí),迭代器在整個(gè) ES6 中是用于數(shù)據(jù)和循環(huán)的新標(biāo)準(zhǔn)。
但是,這還不是生成器能做的事情的全部。這甚至有可能不是他們做的最重要的事情。
這里是一個(gè) JS 代碼,我寫(xiě)了一個(gè) while 的 back 部分。
};
})
});
});
});
});
可能這看起來(lái)和你代碼的一部分比較相像。 異步 API 通常情況下需要一個(gè)回調(diào),這就意味著你做一些事情時(shí)要寫(xiě)一個(gè)額外的匿名函數(shù)。所以如果你有一部分代碼做三件事情,而不是三行代碼,你是在看三個(gè)縮進(jìn)層次的代碼。
這里有一些我已經(jīng)寫(xiě)好的 JS 代碼:
}).on('close', function () {
done(undefined, undefined);
}).on('error', function (error) {
done(error);
});
異步 API 有錯(cuò)誤處理的約定,但不是使用異常。不同的 API 有不同的約定。在他們中的大多數(shù)中,錯(cuò)誤在默認(rèn)情況下被默默地刪除。其中有一些,即使是普通的圓滿完成,在默認(rèn)情況下都會(huì)被刪除。
直到現(xiàn)在,這些問(wèn)題都只是簡(jiǎn)單的轉(zhuǎn)畫(huà)為我們進(jìn)行異步編程的代價(jià)了。我們已經(jīng)開(kāi)始接受異步代碼了,他們只是看起來(lái)不是像同步代碼那樣美好和簡(jiǎn)單。
生成器提供了新的希望,可以不用這樣做的。
Q.async( ) 是一個(gè)試驗(yàn)性的嘗試。其使用迭代器來(lái)生成類似于同步代碼的異步代碼。
舉個(gè)例子:
//
function makeNoise() {
shake();
rattle();
roll();
}
// Asynchronous code to make some noise.
// Returns a Promise object that becomes resolved
// when we're done making noise.
function makeNoise_async() {
return Q.async(function* () {
yield shake_async();
yield rattle_async();
yield roll_async();
});
}
主要的區(qū)別在于,異步版本必須在每個(gè)需要調(diào)用異步函數(shù)的地方添加 yield 關(guān)鍵字。
在 Q.async 版本中添加一點(diǎn)小東西,如 if 語(yǔ)句或 try/ catch 塊,與在普通同步版本中添加是完全相同的。相比于編寫(xiě)異步代碼的其他方式,有種不是在學(xué)習(xí)一個(gè)全新的語(yǔ)言的感覺(jué)。
如果你已經(jīng)遠(yuǎn)遠(yuǎn)得到了這些東西,你可能會(huì)喜歡上 James Long 的在這個(gè)話題上面的非常詳細(xì)的博客。
所以生成器指出了一個(gè)更適合人類大腦的新的異步編程模型。這項(xiàng)工作正在進(jìn)行中。除其他事項(xiàng)外,更好的語(yǔ)法可能有所幫助。
異步函數(shù)的提出,建立在雙方的承諾和生成器的基礎(chǔ)上,并從在 C#類似的功能中采取靈感,這些都是 ES7 要做的事情。
在服務(wù)器端,我們現(xiàn)在可以在 io.js 中使用 ES6 生成器。
如果你啟用 --harmony 選項(xiàng),我們?cè)?Node 中可也已使用 ES6 生成器。
在瀏覽器中,現(xiàn)今只有火狐27版本以上以及谷歌瀏覽器39版本以上的支持 ES6 生成器。為了在現(xiàn)今的網(wǎng)絡(luò)上面使用生成器,你將需要使用 Babel 或者 Traceur 來(lái)將你的 ES6 的代碼翻譯為網(wǎng)絡(luò)友好的 ES5 代碼。
一些重要的事件值得了解:生成器是由布倫丹·艾希首次在 JS 上實(shí)現(xiàn)的。布倫丹·艾希的設(shè)計(jì)是緊緊跟隨由 Icon 啟發(fā)的 Python 生成器。他們?cè)缭?006年就運(yùn)用在火狐 2.0 版本上了。但是標(biāo)準(zhǔn)化的道路是崎嶇不平的,而且語(yǔ)法和行為在這個(gè)過(guò)程中改變了很多。ES6 生成器是由編程黑客溫格安迪在火狐瀏覽器和谷歌瀏覽器中實(shí)現(xiàn)的。這項(xiàng)工作是由 Bloomberg 贊助的。
關(guān)于生成器還有更多的說(shuō)法。我們沒(méi)有包含 .throw() 和 .return() 方法,可選的參數(shù) .next(),或 yield* 表達(dá)式語(yǔ)法。但我認(rèn)為這個(gè)帖子已經(jīng)很長(zhǎng)了,且現(xiàn)在已經(jīng)足夠撲朔迷離了。像生成器本身,我們應(yīng)該停下來(lái)休息一下。