【譯】ES6 生成器 - 1. ES6 生成器基礎(chǔ)

原文地址: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() 返回 nullfalse 或其他可以讓你知道數(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),也就是說返回的最后的 value42。

把上面的內(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 返回。donefalse 時,會自動提取 value 屬性賦值給迭代變量(上例中為 v)。一旦 donetrue,循環(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)境),可能會思考下面的問題:

  1. 異常如何處理?
  2. 一個生成器能夠調(diào)用另一個嗎?
  3. 異步代碼怎么應(yīng)用生成器?

這些問題,以及其他更多的問題,將會在該系列文章中討論,所以,請繼續(xù)關(guān)注!


該系列文章共有4篇,這是第一篇,如有時間,其他3篇也會在近期陸續(xù)翻譯出來。

ES6 Generators: Complete Series

  1. The Basics Of ES6 Generators
  2. Diving Deeper With ES6 Generators
  3. Going Async With ES6 Generators
  4. 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)中。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容