原文地址:https://davidwalsh.name/es6-generators
作者 Kyle Simpson,發(fā)布于 2014年7月21日
生成器(generator),作為一種新的函數(shù),是JavaScript ES6 帶來的最令人興奮的新特性之一。名字或許有點陌生,不過初步了解之后你會發(fā)現(xiàn),它的行為更加陌生。本文的目的是幫你了解生成器,并且讓你認(rèn)識到為什么對于 JS 的未來而言它是如此重要。
執(zhí)行-結(jié)束
首先讓我們來看下它相較于普通函數(shù)“執(zhí)行-結(jié)束”模式的不同之處。
不知道你是否注意過,對于函數(shù)你一直以來的看法就是:一旦函數(shù)開始執(zhí)行,它就會一直執(zhí)行下去直到結(jié)束,這個過程中其他的 JS 代碼無法執(zhí)行。
示例:
setTimeout(function(){
console.log("Hello World");
},1);
function foo() {
// 注意:不要做這樣瘋狂的長時間運行的循環(huán)
for (var i=0; i<=1E10; i++) {
console.log(i);
}
}
foo();
// 0..1E10
// "Hello World"
在上面例子中,for 循環(huán)會執(zhí)行相當(dāng)長的時間才會結(jié)束,至少超過1毫秒,但是定時器回調(diào)函數(shù)中的 console.log(..) 語句并不能在 foo() 函數(shù)執(zhí)行過程中打斷它,所以它會一直等在后面(在事件循環(huán)隊列上),直到函數(shù)執(zhí)行結(jié)束。
如果 foo() 的執(zhí)行可以被打斷呢?那豈不給我們的程序帶來了災(zāi)難?
這就是多線程編程帶來的挑戰(zhàn)(噩夢),不過還好,在 JavaScript 領(lǐng)域我們不用擔(dān)心這種事情,因為 JS 始終是單線程的(同時只會有一個指令或函數(shù)在執(zhí)行)。
注意:Web Worker 機制可以將 JS 程序的一部分在一個單獨的線程中執(zhí)行,與 JS 主程序并行。之所以說這不會帶來多線程的問題,是因為這兩個線程可以通過普通的異步事件來彼此通信,仍然在事件循環(huán)的一次執(zhí)行一個的行為模式下。
執(zhí)行-停止-執(zhí)行
ES6 生成器(generator)是一種不同類型的函數(shù),可以在執(zhí)行過程中暫停若干次,并在稍后繼續(xù)執(zhí)行,使得其他代碼可以在其暫停過程中得到執(zhí)行。
如果你對并發(fā)或線程編程有所了解,你可能聽過“協(xié)作(cooperative)”這個詞,意思是一個進程(這里指函數(shù))可以自主選擇什么時間進行中斷,從而可以與其他代碼協(xié)作。與之相對的是“搶占”,意思是一個進程/函數(shù)可以在外部被打斷。
從并發(fā)行為上來說,ES6 生成器是“協(xié)作的”。在生成器函數(shù)內(nèi)部,可以通過 yield 關(guān)鍵字來暫停函數(shù)的執(zhí)行。不能在生成器外部停止其執(zhí)行;只能是生成器內(nèi)部在遇到 yield 時主動停止。
不過,在生成器通過 yield 暫停后,它不能自己繼續(xù)執(zhí)行。需要通過外部控制來讓生成器重新執(zhí)行。我們會花一點時間來闡述這個過程。
所以,基本上,一個生成器函數(shù)可以停止執(zhí)行和被重新啟動任意多次。實際上,可以通過無限循環(huán)(如臭名昭著的 while (true) { .. })來使得一個生成器函數(shù)永遠(yuǎn)不終止。盡管在通常的 JS 編程中這是瘋了或者出錯了,但對于生成器函數(shù)這卻會是非常合理的,并且有時候就是你需要的!
更重要的是,生成器函數(shù)執(zhí)行過程中的控制并不僅僅是停止和啟動,在這個過程中還實現(xiàn)了生成器函數(shù)內(nèi)外的雙向消息傳遞。對于普通函數(shù),是在最開始執(zhí)行時獲得參數(shù),最后通過 return 返回值。而在生成器函數(shù)中,可以在每個 yield 處向外發(fā)送消息,在每次重新啟動時得到外部返回的消息。
語法!
讓我們開始深入分析這全新和令人興奮的生成器函數(shù)的語法。
首先,新的聲明語法:
function *foo() {
// ..
}
注意到 * 了沒?看起來有點陌生和奇怪吧。對于了解其他語言的人來說,這看起來很像是一個函數(shù)的指針。但是別被迷惑了!這里只是用于標(biāo)記特殊的生成器函數(shù)類型。
你可能看過其他文章/文檔使用了 function* foo() { } 而不是 function *f00() { }(* 的位置有所不同)。兩種都是合法的,不過最近我認(rèn)為 function *foo() { } 更準(zhǔn)確些,所以我后面會使用這種形式。
下面,我們來討論下生成器函數(shù)的內(nèi)容。大多數(shù)情況下,生成器函數(shù)就像是普通的 JS 函數(shù)。在生成器的 內(nèi)部 只有很少的新的語法需要學(xué)習(xí)。
我們主要的新玩具,前面也提到過,就是 yield 關(guān)鍵字。yield __ 被稱為“yield 表達(dá)式”(而非語句),因為生成器重新執(zhí)行時,會得到一個返回給生成器的值,這個值會作為 yield __ 表達(dá)式的值使用。
示例:
function *foo() {
var x = 1 + (yield "foo");
console.log(x);
}
在執(zhí)行到 yield "foo" 這里時,生成器函數(shù)暫停執(zhí)行,"foo" 會被發(fā)送到外部,而(如果)等到生成器重新執(zhí)行時,不管被傳入了什么值,都會作為這個表達(dá)式的結(jié)果值,進而與 1 相加后賦值給變量 x。
看出來雙向通信了嗎?生成器將 "foo" 發(fā)送到外部,暫停自身的執(zhí)行,然后在未來某一時間點(可能是馬上,也可能是很久之后!),生成器被重新啟動并傳回來一個值。這看起來就像是 yield 關(guān)鍵字產(chǎn)生了一個數(shù)據(jù)請求。
在任何使用表達(dá)式的位置,都可以在表達(dá)式/語句中只使用 yield,這就像是對外發(fā)送了 undefinded 值。如:
// 注意:這里的 `foo(..)` 不是生成器函數(shù)??!
function foo(x) {
console.log("x: " + x);
}
function *bar() {
yield; // 只是暫停
foo( yield ); // 暫停,等待傳入一個參數(shù)給 `foo(..)`
}
生成器迭代器
“生成器迭代器”,很拗口是不是?
迭代器是一種特殊的行為,或者說設(shè)計模式,指的是我們從一個有序的值的集合中通過調(diào)用 next() 每次取出一個值。想象一個迭代器,對應(yīng)一個有五個值的數(shù)組:[1,2,3,4,5]。第一次調(diào)用 next() 返回 1,第二次調(diào)用 next() 返回 2,以此類推。在所有的值返回后,next() 返回 null 或 false 或其他可以讓你知道數(shù)據(jù)容器中的所有值已被遍歷的信號。
我們在外部控制生成器函數(shù)的方式,就是構(gòu)造一個 生成器迭代器 并與之交互。這聽起來比實際情況要復(fù)雜。來看下面的例子:
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
為了獲得生成器函數(shù) *foo() 的值,我們需要構(gòu)造一個迭代器。怎么做呢?很簡單!
var it = foo();
噢!所以,像一般函數(shù)那樣調(diào)用生成器函數(shù),其實并沒有執(zhí)行其內(nèi)部。
這有點奇怪是吧。你可能還在想,為什么不是 var it = new foo();。不過很遺憾,語法背后的原因有點復(fù)雜,超出了我們這里討論的范圍。
現(xiàn)在,為了遍歷我們的構(gòu)造器函數(shù),只需要:
var message = it.next();
這會從 yield 1 語句那里得到 1,但這并不是唯一返回的東西。
console.log(message); // { value:1, done:false }
實際上每次調(diào)用 next() 會返回一個對象,返回對象包含一個對應(yīng) yield 返回值的 value 屬性,以及一個表示生成器函數(shù)是否已經(jīng)完全執(zhí)行完畢的布爾型的 done 屬性。
繼續(xù)迭代過程:
console.log( it.next() ); // { value:2, done:false }
console.log( it.next() ); // { value:3, done:false }
console.log( it.next() ); // { value:4, done:false }
console.log( it.next() ); // { value:5, done:false }
有意思的是,done 屬性在獲取到 5 這個值時仍為 false。這是因為從 技術(shù)上 講,生成器函數(shù)的執(zhí)行還未結(jié)束。我們還需要最后一次調(diào)用 next(),這時如果我們傳入一個值,它會被用作表達(dá)式 yield 5 的結(jié)果。然后 生成器函數(shù)才會結(jié)束。
所以,現(xiàn)在:
console.log( it.next() ); // { value:undefined, done:true }
生成器函數(shù)的最后一個返回結(jié)果表示函數(shù)執(zhí)行結(jié)束,但沒有值返回(因為所有的 yield 語句都已執(zhí)行)。
你可能會想,如果在生成器函數(shù)中使用 return,返回的值會在 value 屬性中嗎?
是...
function *foo() {
yield 1;
return 2;
}
var it = foo();
console.log( it.next() ); // { value:1, done:false }
console.log( it.next() ); // { value:2, done:true }
...也不是
依賴生成器的 return 值不是個好主意,因為當(dāng)生成器函數(shù)在 for .. of 循環(huán)(見下文)中進行迭代時,最后的 return 值會被丟棄。
下面,我們來完整地看下生成器函數(shù)在迭代時的數(shù)據(jù)傳入和傳出:
function *foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var it = foo( 5 );
// 注意:這里沒有向 `next()` 傳入任何值
console.log( it.next() ); // { value:6, done:false }
console.log( it.next( 12 ) ); // { value:8, done:false }
console.log( it.next( 13 ) ); // { value:42, done:true }
可以看到,通過迭代初始化時調(diào)用的 foo( 5 ) 仍然可以進行傳參(對應(yīng)例子中的 x),這和普通函數(shù)相同,會使 x 的值為 5。
第一個 next(..) 調(diào)用,沒有傳入任何值。為什么?因為沒有對應(yīng)的 yield 表達(dá)式來接收傳入的值。
不過即使第一次調(diào)用時傳入了值,也不會有什么壞事發(fā)生。傳入的值只是被丟棄了而已。ES6 規(guī)定這種情況下生成器函數(shù)要忽略沒有用到的值。(注意:在實際寫代碼的時候,最新版的 Chrome 和 FF 應(yīng)該沒問題,不過其他瀏覽器可能不是完全兼容的,或許會在這種情況下拋出異常。)
語句 yield (x + 1) 向外發(fā)送 6。第二個調(diào)用 next(12) 向正在等待狀態(tài)的 yield (x + 1) 表達(dá)式發(fā)送了 12,所以 y 的值為 12 * 2,也就是 24。然后 yield (y / 3)(yield (24 / 3))向外發(fā)送值 8。第三個調(diào)用 next(13) 向表達(dá)式 yield (y / 3) 發(fā)送了 13,使得 z 的值為 13。
最終,return (x + y + z) 是 return (5 + 24 + 13),也就是說返回的最后的 value 是 42。
把上面的內(nèi)容多看幾遍。對于大多數(shù)人來說,最初看的時候都會感覺很奇怪。
for..of
ES6 也在語義層面上加強了迭代模式,它提供了對迭代器執(zhí)行的直接支持:for..of 循環(huán)。
示例:
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (var v of foo()) {
console.log( v );
}
// 1 2 3 4 5
console.log( v ); // 仍舊是 `5`,而不是 `6` :(
可以看到,foo() 創(chuàng)建的迭代器會被 for..of 循環(huán)自動捕獲,然后被自動進行遍歷,每次返回一個值,直到 done:true 返回。done 為 false 時,會自動提取 value 屬性賦值給迭代變量(上例中為 v)。一旦 done 是 true,循環(huán)迭代終止(也不會處理最后返回的 value,如果有的話)。
就像上文提到過的那樣,for..of 循環(huán)忽略并丟棄了最后的 return 6 的值。所以,由于沒有暴露 next() 調(diào)用,還是不要在像上面那種情況下使用for..of 循環(huán)。
總結(jié)
OK,以上就是生成器的基礎(chǔ)知識了。如果還是有點懵,不用擔(dān)心。所有人一開始都是這樣的!
很自然地,你會想這個外來的新玩具在自己的代碼中實際會怎么使用。其實,有關(guān)生成器還有很多的東西。我們只是翻開了封面而已。所以,在發(fā)現(xiàn)生成器是/將會多么強大之前,我們還得更進一步學(xué)習(xí)。
在你試著玩過上面的代碼片段之后(試試 Chrome 最新版或 FF 最新版,或者帶有 --harmony 標(biāo)記的 node 0.11+ 環(huán)境),可能會思考下面的問題:
- 異常如何處理?
- 一個生成器能夠調(diào)用另一個嗎?
- 異步代碼怎么應(yīng)用生成器?
這些問題,以及其他更多的問題,將會在該系列文章中討論,所以,請繼續(xù)關(guān)注!
該系列文章共有4篇,這是第一篇,如有時間,其他3篇也會在近期陸續(xù)翻譯出來。
ES6 Generators: Complete Series
- The Basics Of ES6 Generators
- Diving Deeper With ES6 Generators
- Going Async With ES6 Generators
- Getting Concurrent With ES6 Generators
另外,有關(guān) for..of 的部分,其實有個細(xì)節(jié)文章沒有解釋。for..if 接收的并不是迭代器(實現(xiàn)了 iterator 接口,也就是有 next() 方法),而應(yīng)該是實現(xiàn)了 iterable 接口的對象。
之所以生成器函數(shù)調(diào)用后的返回值可以用于 for..of,是由于得到的生成器對象同時支持了 iterator 接口和 iterable 接口。
iterable 接口對應(yīng)一個特殊的方法,調(diào)用后返回一個迭代器,對于生成器對象而言,這個接口方法返回的其實就是對象自身。
由于同時支持了兩個接口,所以生成器函數(shù)返回的生成器對象既能直接調(diào)用 next(),也可以用于 for..in 循環(huán)中。