今天,我們將要討論的是 ES6 中最奇妙的特性。
為何稱之為"奇妙"呢?對于初學者而言,這個特性與現(xiàn)有 JavaScript 中的內(nèi)容看起來是如此這般的格格不入。從某種角度而言,它將由內(nèi)而外地改變語言的常見行為。如果這還不算奇妙,那還有什么能算呢?
不僅如此,這個特性可以大幅度地簡化現(xiàn)有代碼,并神奇地解決"callback hell" 的問題。
接下來讓我們來深入了解一下這個奇妙的特性吧。
ES6 生成器簡介
什么是生成器(Generator)?
先來看一個例子:
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!";
}
這是一段會說話的貓中的代碼,也許是當今互聯(lián)網(wǎng)上最重要的一類應用。(點擊鏈接,與這只貓互動。當你感到困惑的時候,再回來看看這篇文章中的解釋。)
生成器(Generator)看起來像是一種函數(shù),對嗎?它確實被稱之為生成器函數(shù),且與函數(shù)有很多相似之處。但是兩者有以下兩點區(qū)別:
- 普通函數(shù)用
function聲明。生成器函數(shù)則用function *。 - 在生成器函數(shù)內(nèi)部,關(guān)鍵詞
yield是一種類似return的語法。區(qū)別就在于函數(shù)(甚至生成器函數(shù))只能返回一次,但是生成器函數(shù)可以yield多次。yield表達式將生成器的執(zhí)行過程掛起,隨后可以被恢復。
所以這是一個普通函數(shù)與生成器函數(shù)之間比較大的區(qū)別。普通函數(shù)無法暫停自身的執(zhí)行,而生成器函數(shù)可以。
生成器是做什么的?
當你調(diào)用 quips() 生成器函數(shù)的時候發(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 }
你可能對普通函數(shù)以及它們是如何運行已經(jīng)非常了解。當你調(diào)用它們的時候,就馬上開始運行,直到遇到return語句或者拋出異常。這對于 JavaScript 程序員而言是再熟悉不過的了。
調(diào)用生成器看起來(和普通函數(shù))沒有區(qū)別:quips("jorendorff")。但是當你調(diào)用一個生成器的時候,它還沒有開始運行。反之,它返回了一個暫停的生成器對象(上述例子中的iter)。你可以把生成器對象當做一個函數(shù)調(diào)用,但是立即凍結(jié)了。具體而言,它在運行生成器函數(shù)頂端第一行代碼之前就已經(jīng)凍結(jié)了。
每次調(diào)用生成器對象的.next()方法,函數(shù)就會將自身解凍然后運行直到遇到下一個yield表達式。
這也就是為什么在上面我們每次調(diào)用iter.next()的時候,都會得到一個不同的字符串值。這些值都是由quips()函數(shù)體內(nèi)的yield表達式生成的。
在最后一次調(diào)用iter.next()時,就到了生成器函數(shù)的末尾,所以返回值中的.done字段值為true。函數(shù)執(zhí)行完就如同返回了一個undefined,這也就是為什么返回值中的.value字段的值為undefined。
現(xiàn)在可以回過頭來看會說話的貓的 demo。嘗試著把yield放到一個循環(huán)中,會發(fā)生什么?
就技術(shù)角度而言,每當一個生成器執(zhí)行 yield 操作的時候,它的棧結(jié)構(gòu)內(nèi)的本地變量,參數(shù),臨時變量以及生成器內(nèi)部的執(zhí)行位置都會被移出棧。但是生成器對象本身維持了一個對棧結(jié)構(gòu)的引用或者拷貝,所以之后的 .next() 調(diào)用可以重新激活生成器隨后繼續(xù)執(zhí)行。
值得注意的是,生成器不是線程。支持線程的語言中,多段不同的代碼可以在同一時候運行,這經(jīng)常會導致竟態(tài)條件、不確定性以及不錯的性能提升。生成器則完全不同。當生成器運行的時候,它會在叫做 caller 的同一個線程中運行。執(zhí)行的順序是有序、確定的,并且永遠不會產(chǎn)生并發(fā)。不同于系統(tǒng)的線程,生成器只會在其內(nèi)部用到 yield 的時候才會被掛起。
好了。既然已經(jīng)知道生成器是什么,也看到生成器是如何運行、暫停然后恢復執(zhí)行。那么問題來了,這個奇怪的功能到底有什么用處呢?
生成器是迭代器
在上一篇文章中,我們了解到 ES6 迭代器不僅僅是一個單獨的內(nèi)建類。同時也是對語言的一個擴展點。你可以通過實現(xiàn)兩個方法:[Symbol.iterator]() 和 .next() 創(chuàng)建自己的迭代器。
但是實現(xiàn)一個接口總歸不是一件小事。讓我們來看看如何在實踐中實現(xiàn)一個迭代器。舉個例子,來創(chuàng)建一個簡單的 range 迭代器 -- 可以從一個數(shù)字計數(shù)到另一個,類似C 語言中經(jīng)典的 for(;;) 循環(huán)。
// 以下代碼將會輸出三次 Ding!
for (var value of range(0, 3)) {
alert("Ding! at floor #" + value);
}
以下是一個使用 ES6 class 的解決方案。(如果對 class 語法不是完全了解,不要擔心,我們將會在之后的另一篇文章中介紹)
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};
}
}
}
// 返回一個新的迭代器,從 'start' 計數(shù)到 'stop'.
function range (start, stop) {
return new RangeIterator(start, stop);
}
這是像 Java 或 Swift 那樣實現(xiàn)的一個迭代器。不是很糟糕,但是也不是完全沒有問題。那么這段代碼會有 bug 嗎?不好說。它看起來與我們一開始要實現(xiàn)的 for(;;) 循環(huán)沒有絲毫相像之處:迭代器的協(xié)議迫使我們拆解了循環(huán)。
此時你也許已經(jīng)對迭代器不是那么感興趣了。他們也許用起來非常不錯,但是看起來卻難以實現(xiàn)。
也許你不會建議為了讓迭代器的構(gòu)建更為容易而使用我們介紹的這種 JavaScript 語言中新的陌生而復雜的控制流程結(jié)構(gòu)。但是既然我們有了生成器,那么為什么在此去使用它呢?一起來試試吧:
function* range(start, stop) {
for (var i = start; i < stop; i++)
yield i;
}
上述使用 generator 的4行代碼可以替代之前用23行代碼的包含了完整的 RangeIterator 類的range()實現(xiàn)。這大概是因為 generator 也屬于迭代器。所有的生成器都內(nèi)置實現(xiàn)了 .next() 和 [Symbol.iterator]() 方法。你只需編寫循環(huán)部分的邏輯。
不使用生成器實現(xiàn)迭代器就像完全用被動語句寫一篇很長的郵件。如果無法簡練地表達,那么說出來的話可能會相當晦澀難懂。由于 RangeIterator 并沒有用循環(huán)的語法來描述一個循環(huán)的功能,從而令人覺得它額外的冗長、怪異。相反的,生成器才是這個問題的答案。
將其當做迭代器,還能如何發(fā)揮生成器的能力呢?
令任意對象可迭代。只需寫一個生成器函數(shù)遍歷這個對象,在此過程中把每個值
yield。然后將這個生成器函數(shù)作為對象的[Symbol.iterator]方法。簡化數(shù)組構(gòu)建的函數(shù)。假設(shè)你有一個函數(shù),每次調(diào)用的時候都返回一個結(jié)果的數(shù)組,比如這個:
// 將一個一維數(shù)組 'icons' 根據(jù) '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);
}
}
唯一的不同之處在于,函數(shù)返回的是一個迭代器而不是將所有結(jié)果計算好一次性返回整個數(shù)組。結(jié)果是按需逐一計算出來的。
異常大小的結(jié)果。你無法創(chuàng)建一個無窮大小的數(shù)組,但是可以返回一個可以生成無窮序列的生成器,并且每次調(diào)用都可以從中獲取任意數(shù)量的值。
重構(gòu)復雜的循環(huán)。你有寫過又大又丑的函數(shù)嗎?想要把它拆分為兩個更為簡單的部分嗎?相信生成器會成為你重構(gòu)工具箱中的一把利刃。當面對一個復雜的循環(huán)時,你拆分出那段產(chǎn)生數(shù)據(jù)的代碼,然后將其變成一個獨立的生成器函數(shù)。然后用
for修改循環(huán)(var data of myNewGenerator(args))。創(chuàng)建可迭代的工具。ES6 沒有提供一個可以用于過濾、映射以及針對任意數(shù)據(jù)集進行操作的擴展庫。但是借助生成器,就可以用很少幾行代碼構(gòu)建這一類工具。
例如,假設(shè)你需要一個類似 Array.prototype.filter 的應用于 DOM NodeLists 而不僅僅是數(shù)組的工具。很簡單:
function* filter(test, iterable) {
for (var item of iterable) {
if (test(item))
yield item;
}
}
那么生成器真的有用嗎?當然。這是極其簡單的、用于實現(xiàn)自定義迭代器的方法,而且根據(jù) ES6 的標準,迭代器是最新的用于數(shù)據(jù)和循環(huán)的標準。
但生成器所能做的并不僅僅局限于此,也許這也不是生成器可以做的最重要的事。
生成器與異步代碼
這是一段我之前寫的代碼:
};
})
});
});
});
});
也許你也在自己的代碼中見到過類似這樣的部分。異步的 API 通常需要一個回調(diào),也就意味著沒做一些事情就需要編寫一個額外的異步函數(shù)。所以如果你做了三件事,就會看到有三層縮進的代碼而非三行。
下面也是我曾寫過的一段代碼:
}).on('close', function () {
done(undefined, undefined);
}).on('error', function (error) {
done(error);
});
異步接口有錯誤處理的機制,而沒有異常處理。對于不同的接口而言,有著不同的約定俗成。就其中大部分而言,錯誤是默認被靜默拋出的。而其中有一些的成功提示都是默認的。
以上這些問題都是我們?yōu)楫惒骄幊趟冻龅拇鷥r。我們正在慢慢接受異步代碼比如其等效的同步代碼簡潔美觀的事實。
生成器則給了人們不用這樣書寫代碼的新的希望。
Q.async() 是一個實驗性的結(jié)合 promise 使用生成器來生成與異步代碼等效的同步代碼的庫。例如:
// 制造“噪音”的同步代碼
function makeNoise() {
shake();
rattle();
roll();
}
// 制造“噪音”的異步代碼
// 當我們產(chǎn)生“噪音”后
// 返回一個 resolved 狀態(tài)的 Promise 對象
function makeNoise_async() {
return Q.async(function* () {
yield shake_async();
yield rattle_async();
yield roll_async();
});
}
上述兩段代碼最主要的區(qū)別就在于異步版本的代碼必須在調(diào)用異步函數(shù)的地方添加 yield 關(guān)鍵詞。
在 Q.async 版本的代碼中增加一個 if 或者 try/catch 語句與添加到普通的同步版本中幾乎一樣。相比其他的編寫異步代碼的方式,這種方式更讓人感覺不是在學習一門完全新的語言。
如果你已經(jīng)到了這一步,可以嘗試欣賞一下 James Long 的關(guān)于這個話題一篇比較詳細的文章。
所以生成器指明了一種更為適合人類大腦的新的異步編程模型。相關(guān)的工作還在繼續(xù)進行當中。相比其他東西,更好的語法會更有幫助。一種構(gòu)建于 promise 和 generator 之上的更好的異步函數(shù)提案,將會在 ES7 中出現(xiàn),這一提案借鑒了 C#中一些相似的特性。
什么時候可以使用這些瘋狂的特性
在服務(wù)端,已經(jīng)可以在 io.js (或者用 --harmony 啟動的 Node) 中使用 ES6 的生成器。
而在瀏覽器中,目前為止只有 Firefox27+ 和 Chrome 39+ 支持 ES6 的生成器。若要在瀏覽器中使用之,可以使用 Babel 或者 Traceur 將 ES6 的代碼轉(zhuǎn)換為瀏覽器友好的 ES5代碼。
最初,Brendan Eich 借鑒 Python 實現(xiàn)了 JavaScript 中的生成器,而 Python 中的生成器則是受 Icon 啟發(fā)實現(xiàn)的。他們早在 2006年發(fā)布的 Firefox 2.0中就實現(xiàn)了這一特性。然而,通向標準化的路是崎嶇的,一路上語法與行為都發(fā)生了不少的變化。ES6 生成器在 Firefox 和 Chrome 中都是由編譯器黑客 Andy Wingo 實現(xiàn)的。這一工作由Bloomberg贊助。
yield;
關(guān)于生成器還有很多內(nèi)容,我們沒有提到 .throw() 和 .return() 方法,.next() 方法的可選參數(shù)以及 yield* 表達式語法。但是我認為這篇文章到現(xiàn)在已經(jīng)足夠長,看著可能少許有點累了。就像生成器他們一樣,我們需要稍作歇息,之后找個時間繼續(xù)講解。