【譯】ES6 生成器 - 1. ES6 生成器基礎

原文地址:https://davidwalsh.name/es6-generators


作者 Kyle Simpson,發布于 2014年7月21日

生成器(generator),作為一種新的函數,是JavaScript ES6 帶來的最令人興奮的新特性之一。名字或許有點陌生,不過初步了解之后你會發現,它的行為更加陌生。本文的目的是幫你了解生成器,并且讓你認識到為什么對于 JS 的未來而言它是如此重要。

執行-結束

首先讓我們來看下它相較于普通函數“執行-結束”模式的不同之處。

不知道你是否注意過,對于函數你一直以來的看法就是:一旦函數開始執行,它就會一直執行下去直到結束,這個過程中其他的 JS 代碼無法執行。

示例:

setTimeout(function(){
    console.log("Hello World");
},1);

function foo() {
    // 注意:不要做這樣瘋狂的長時間運行的循環
    for (var i=0; i<=1E10; i++) {
        console.log(i);
    }
}

foo();
// 0..1E10
// "Hello World"

在上面例子中,for 循環會執行相當長的時間才會結束,至少超過1毫秒,但是定時器回調函數中的 console.log(..) 語句并不能在 foo() 函數執行過程中打斷它,所以它會一直等在后面(在事件循環隊列上),直到函數執行結束。

如果 foo() 的執行可以被打斷呢?那豈不給我們的程序帶來了災難?

這就是多線程編程帶來的挑戰(噩夢),不過還好,在 JavaScript 領域我們不用擔心這種事情,因為 JS 始終是單線程的(同時只會有一個指令或函數在執行)。

注意:Web Worker 機制可以將 JS 程序的一部分在一個單獨的線程中執行,與 JS 主程序并行。之所以說這不會帶來多線程的問題,是因為這兩個線程可以通過普通的異步事件來彼此通信,仍然在事件循環的一次執行一個的行為模式下。

執行-停止-執行

ES6 生成器(generator)是一種不同類型的函數,可以在執行過程中暫停若干次,并在稍后繼續執行,使得其他代碼可以在其暫停過程中得到執行。

如果你對并發或線程編程有所了解,你可能聽過“協作(cooperative)”這個詞,意思是一個進程(這里指函數)可以自主選擇什么時間進行中斷,從而可以與其他代碼協作。與之相對的是“搶占”,意思是一個進程/函數可以在外部被打斷。

從并發行為上來說,ES6 生成器是“協作的”。在生成器函數內部,可以通過 yield 關鍵字來暫停函數的執行。不能在生成器外部停止其執行;只能是生成器內部在遇到 yield 時主動停止。

不過,在生成器通過 yield 暫停后,它不能自己繼續執行。需要通過外部控制來讓生成器重新執行。我們會花一點時間來闡述這個過程。

所以,基本上,一個生成器函數可以停止執行和被重新啟動任意多次。實際上,可以通過無限循環(如臭名昭著的 while (true) { .. })來使得一個生成器函數永遠不終止。盡管在通常的 JS 編程中這是瘋了或者出錯了,但對于生成器函數這卻會是非常合理的,并且有時候就是你需要的!

更重要的是,生成器函數執行過程中的控制并不僅僅是停止和啟動,在這個過程中還實現了生成器函數內外的雙向消息傳遞。對于普通函數,是在最開始執行時獲得參數,最后通過 return 返回值。而在生成器函數中,可以在每個 yield 處向外發送消息,在每次重新啟動時得到外部返回的消息。

語法!

讓我們開始深入分析這全新和令人興奮的生成器函數的語法。

首先,新的聲明語法:

function *foo() {
    // ..
}

注意到 * 了沒?看起來有點陌生和奇怪吧。對于了解其他語言的人來說,這看起來很像是一個函數的指針。但是別被迷惑了!這里只是用于標記特殊的生成器函數類型。

你可能看過其他文章/文檔使用了 function* foo() { } 而不是 function *f00() { }* 的位置有所不同)。兩種都是合法的,不過最近我認為 function *foo() { } 更準確些,所以我后面會使用這種形式。

下面,我們來討論下生成器函數的內容。大多數情況下,生成器函數就像是普通的 JS 函數。在生成器的 內部 只有很少的新的語法需要學習。

我們主要的新玩具,前面也提到過,就是 yield 關鍵字。yield __ 被稱為“yield 表達式”(而非語句),因為生成器重新執行時,會得到一個返回給生成器的值,這個值會作為 yield __ 表達式的值使用。

示例:

function *foo() {
    var x = 1 + (yield "foo");
    console.log(x);
}

在執行到 yield "foo" 這里時,生成器函數暫停執行,"foo" 會被發送到外部,而(如果)等到生成器重新執行時,不管被傳入了什么值,都會作為這個表達式的結果值,進而與 1 相加后賦值給變量 x

看出來雙向通信了嗎?生成器將 "foo" 發送到外部,暫停自身的執行,然后在未來某一時間點(可能是馬上,也可能是很久之后!),生成器被重新啟動并傳回來一個值。這看起來就像是 yield 關鍵字產生了一個數據請求。

在任何使用表達式的位置,都可以在表達式/語句中只使用 yield,這就像是對外發送了 undefinded 值。如:

// 注意:這里的 `foo(..)` 不是生成器函數!!
function foo(x) {
    console.log("x: " + x);
}

function *bar() {
    yield; // 只是暫停
    foo( yield ); // 暫停,等待傳入一個參數給 `foo(..)`
}

生成器迭代器

“生成器迭代器”,很拗口是不是?

迭代器是一種特殊的行為,或者說設計模式,指的是我們從一個有序的值的集合中通過調用 next() 每次取出一個值。想象一個迭代器,對應一個有五個值的數組:[1,2,3,4,5]。第一次調用 next() 返回 1,第二次調用 next() 返回 2,以此類推。在所有的值返回后,next() 返回 nullfalse 或其他可以讓你知道數據容器中的所有值已被遍歷的信號。

我們在外部控制生成器函數的方式,就是構造一個 生成器迭代器 并與之交互。這聽起來比實際情況要復雜。來看下面的例子:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
}

為了獲得生成器函數 *foo() 的值,我們需要構造一個迭代器。怎么做呢?很簡單!

var it = foo();

噢!所以,像一般函數那樣調用生成器函數,其實并沒有執行其內部。

這有點奇怪是吧。你可能還在想,為什么不是 var it = new foo();。不過很遺憾,語法背后的原因有點復雜,超出了我們這里討論的范圍。

現在,為了遍歷我們的構造器函數,只需要:

var message = it.next();

這會從 yield 1 語句那里得到 1,但這并不是唯一返回的東西。

console.log(message); // { value:1, done:false }

實際上每次調用 next() 會返回一個對象,返回對象包含一個對應 yield 返回值的 value 屬性,以及一個表示生成器函數是否已經完全執行完畢的布爾型的 done 屬性。

繼續迭代過程:

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。這是因為從 技術上 講,生成器函數的執行還未結束。我們還需要最后一次調用 next(),這時如果我們傳入一個值,它會被用作表達式 yield 5 的結果。然后 生成器函數才會結束。

所以,現在:

console.log( it.next() ); // { value:undefined, done:true }

生成器函數的最后一個返回結果表示函數執行結束,但沒有值返回(因為所有的 yield 語句都已執行)。

你可能會想,如果在生成器函數中使用 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 值不是個好主意,因為當生成器函數在 for .. of 循環(見下文)中進行迭代時,最后的 return 值會被丟棄。

下面,我們來完整地看下生成器函數在迭代時的數據傳入和傳出:

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 }

可以看到,通過迭代初始化時調用的 foo( 5 ) 仍然可以進行傳參(對應例子中的 x),這和普通函數相同,會使 x 的值為 5

第一個 next(..) 調用,沒有傳入任何值。為什么?因為沒有對應的 yield 表達式來接收傳入的值。

不過即使第一次調用時傳入了值,也不會有什么壞事發生。傳入的值只是被丟棄了而已。ES6 規定這種情況下生成器函數要忽略沒有用到的值。(注意:在實際寫代碼的時候,最新版的 Chrome 和 FF 應該沒問題,不過其他瀏覽器可能不是完全兼容的,或許會在這種情況下拋出異常。)

語句 yield (x + 1) 向外發送 6。第二個調用 next(12) 向正在等待狀態的 yield (x + 1) 表達式發送了 12,所以 y 的值為 12 * 2,也就是 24。然后 yield (y / 3)yield (24 / 3))向外發送值 8。第三個調用 next(13) 向表達式 yield (y / 3) 發送了 13,使得 z 的值為 13

最終,return (x + y + z)return (5 + 24 + 13),也就是說返回的最后的 value42

把上面的內容多看幾遍。對于大多數人來說,最初看的時候都會感覺很奇怪。

for..of

ES6 也在語義層面上加強了迭代模式,它提供了對迭代器執行的直接支持:for..of 循環。

示例:

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() 創建的迭代器會被 for..of 循環自動捕獲,然后被自動進行遍歷,每次返回一個值,直到 done:true 返回。donefalse 時,會自動提取 value 屬性賦值給迭代變量(上例中為 v)。一旦 donetrue,循環迭代終止(也不會處理最后返回的 value,如果有的話)。

就像上文提到過的那樣,for..of 循環忽略并丟棄了最后的 return 6 的值。所以,由于沒有暴露 next() 調用,還是不要在像上面那種情況下使用for..of 循環。

總結

OK,以上就是生成器的基礎知識了。如果還是有點懵,不用擔心。所有人一開始都是這樣的!

很自然地,你會想這個外來的新玩具在自己的代碼中實際會怎么使用。其實,有關生成器還有很多的東西。我們只是翻開了封面而已。所以,在發現生成器是/將會多么強大之前,我們還得更進一步學習。

在你試著玩過上面的代碼片段之后(試試 Chrome 最新版或 FF 最新版,或者帶有 --harmony 標記的 node 0.11+ 環境),可能會思考下面的問題:

  1. 異常如何處理?
  2. 一個生成器能夠調用另一個嗎?
  3. 異步代碼怎么應用生成器?

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


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

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

另外,有關 for..of 的部分,其實有個細節文章沒有解釋。for..if 接收的并不是迭代器(實現了 iterator 接口,也就是有 next() 方法),而應該是實現了 iterable 接口的對象。

之所以生成器函數調用后的返回值可以用于 for..of,是由于得到的生成器對象同時支持了 iterator 接口和 iterable 接口。

iterable 接口對應一個特殊的方法,調用后返回一個迭代器,對于生成器對象而言,這個接口方法返回的其實就是對象自身。

由于同時支持了兩個接口,所以生成器函數返回的生成器對象既能直接調用 next(),也可以用于 for..in 循環中。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容