[譯] Javascript(ES6)Generator 入門

ssshooter發表於2018-07-16

如果你在過去兩到五年中一直在研究 JavaScript,那麼肯定看過關於 GeneratorIterator 的文章。雖然 GeneratorIterator 本質上是相關的,但 Generator 似乎比 Iterator 更令人難以理解。

[譯] Javascript(ES6)Generator 入門

IteratorIterable 物件(如 map,陣列和字串等)實現,我們能夠使用 next() 迭代它們。Iterator 在 Generator,Observable 和 Spread 運算子中廣泛使用。

如果你剛接觸 Iterator,建議先閱讀 Guide to Iterators

可以使用內建的 Symbol.iterator 驗證物件是否符合可迭代要求:

new Map([[1, 2]])[Symbol.iterator]() // MapIterator {1 => 2}
“hi”[Symbol.iterator]() // StringIterator {}
[‘1’][Symbol.iterator]() // Array Iterator {}
new Set([1, 2])[Symbol.iterator]() // SetIterator {1, 2}
複製程式碼

第一次亮相於 ES6 的 Generator 在後續 JavaScript 版本的釋出中並沒有變化,所以 Generator 有可能在將來會繼續保持現在的特性及用法,我們是繞不開它的。雖然 ES7 和 ES8 有一些小更新,但是改變幅度無法與 ES5 到 ES6 相提並論,可以說 ES6 使得 JavaScript 踏出了新的一步。

讀完本文,我相信你一定能充分理解 Generator 的原理。如果你是專業人士,歡迎在回覆中新增評論,一起改進這篇文章。為幫助大家理解程式碼,程式碼中已包含一定註釋。

[譯] Javascript(ES6)Generator 入門

介紹

眾所周知,JavaScript 的函式都會一直執行到 return 或函式結束。但對於 Generator 函式,會一直執行到 遇到 yield 或 return 或函式結束。與一般函式不同,Generator 函式一旦被呼叫,就會返回一個 Generator 物件。這個物件擁有 Generator Iterable,可以使用 next() 方法或 for…of 迴圈迭代它。

Generator 每次呼叫 next(),函式會一直執行到下一個 yield,然後暫停執行。

語法上他們的標誌是一個星號 *function* Xfunction *X 的效果相同。

Generator 函式返回 Generator 物件。要把 Generator 物件賦值到一個變數,才能方便地使用它的 next() 方法。 如果沒有把 Generator 分配給變數,對它呼叫 next() 總是隻會執行到第一個 yield 表示式。

Generator 函式中通常含有 yield 表示式。Generator 函式內的每個 yield 都是下一個執行迴圈開始之前的停止點。每個執行週期都通過 Generator 的 next() 方法觸發。

每次呼叫 next()yield 表示式都會返回包含以下引數的物件。

{ value: 10, done: false } // 假設 yield 的值是 10

  • Value —— yield 關鍵字右側的值,可以是對函式的呼叫、物件等幾乎任何東西。對於空的 yield,返回的是 undefined
  • Done —— 表明 Generator 的狀態,是否可以繼續執行。完成時返回 true,意味著函式已經執行完畢。

(如果你無法理解上面說的是什麼,那下面的例子可能會讓你理解得更清晰……)

[譯] Javascript(ES6)Generator 入門

Generator 函式基礎

**注意:**在上面的例子中,直接訪問 Generator 函式總是執行到第一個 yield。因此,你需要將 Generator 分配給變數才能正確迭代它。

Generator 函式的生命週期

在深入理解之前,讓我們快速瀏覽一下 Generator 函式的生命週期示意圖:

[譯] Javascript(ES6)Generator 入門

Generator 函式的生命週期

每次執行到 yield,Generator 函式都會返回一個物件,該物件包含 yield 產生的值和當前 Generator 函式的狀態。類似地,執行到 return,可以得到 return 的值,並且 done 的狀態為 true。當 done 的狀態為 true 時,意味著 Generator 函式已經執行完畢,後面的 yield 統統無效。

return 後的一切程式碼都會被忽略,包括 yield 表示式。

繼續閱讀深入理解上圖。

把 yield 賦值到一個變數

在的示例中,我們建立了一個帶有 yield 的最基本的 Generator,並獲得了預期的輸出。在下面程式碼中,我們將整個 yield 表示式賦值到一個變數。

[譯] Javascript(ES6)Generator 入門

把 yield 賦值到一個變數

把整個 yield 表示式傳到變數的結果是什麼?Undefined …

為什麼會是 undefined?從第二個 next() 開始,前一個 yield 會被替換為 next 函式的引數。因為例子中的 next 沒有傳入任何值,所以程式判定“前一個 yield 表示式”為 undefined

這是重點中的重點,下面的章節我們將詳細介紹對 next() 傳參的用法。

將引數傳遞給 next() 方法

參考上面的示意圖,我們聊聊關於傳參到 next 函式的事情。這是整個 Generator 使用中最棘手的部分之一

思考以下程式碼,其中 yield 被賦給變數,但這次我們向 next() 傳參。

看看控制檯的輸出,先思考一下,後面會有解釋。

[譯] Javascript(ES6)Generator 入門

將引數傳遞給 next()

說明:

  1. 在呼叫 next(20) 的時候,第一個 yield 前的程式碼都被執行。因為前面已經沒有 yield,傳入的 20 毫無作用。輸出 yield 的 value 為 i*10,也就是 100。因為執行到第一個 yield 停止,所以 const j 未被賦值。
  2. 呼叫 next(10) 時,第一個 yield 的位置被替換為 10,相當於在返回第二個 yield 的 value 前,設定 yield (i * 10) = 10,所以 j 為 50。yield 的 value 為 2 * 50 / 4 = 25
  3. next(5) 用 5 替換第二個 yield,所以 k 為 5。繼續執行 return 語句,返回最後的 yield value (x + y + z) => (10 + 50 + 5) = 65,並且 done 為 true。

這可能對初次接觸 Generator 的讀者有點超綱,但是給自己 5 分鐘,多讀幾遍,就能清楚明白。

Yield 作為其他函式的引數

Yield 在 Generator 中還有大把的用法,我們接著看看下面的程式碼,這是 yield 的其中一個妙用,附帶解釋。

[譯] Javascript(ES6)Generator 入門

Yield 作為其他函式的引數

解釋

  1. 第一個 next() yield(生成) 的 value 為 undefined,因為 yield 表示式無值。
  2. 第二個 next() 生成的 value 為被傳入的 'I am usless',這一步為函式呼叫準備了引數。
  3. 第二個 next() 以 undefined 為引數呼叫了後面的函式。next() 沒有接收引數,意味著上一個 yield 表示式的值為 undefined,所以函式列印出 undefined 並終止執行。

對函式呼叫使用 yield

除了返回普通的值,yield 還可以呼叫函式並返回他的值。看看下面的例子更好理解:

[譯] Javascript(ES6)Generator 入門

對函式呼叫使用 yield

上述程式碼返回了函式返回的物件作為 yield 的 value,然後把 const user 賦值為 undefined,結束執行。

對 Promise 使用 yield

對 promise 使用 yield 與對函式呼叫使用 yield 相似,它會返回一個 promise,我們以此進一步判定操作成功或失敗。看看以下程式碼,瞭解它的使用方法:

[譯] Javascript(ES6)Generator 入門

對 Promise 使用 yield

apiCall 將 promise 作為 yield value 返回,在 2 秒後 resolved 並列印出我們需要的值。

Yield*

Yield 表示式的介紹就告一段落了,接著我們瞭解一下另一個表示式 yield*Yield* 在 Generator 函式中使用時,會把迭代委託到下一個 Generator 函式。簡單來說,會先同步完成 Yield* 表示式中的 Generator 函式,再繼續執行外層函式。

讓我們看看下面的程式碼和解釋,以便更好地理解。此程式碼來自 MDN Web 文件。

[譯] Javascript(ES6)Generator 入門

Yield* 基礎

解釋

  1. 呼叫第一個 next(),產生的值為1。
  2. 第二個 next() 呼叫的是 yield* 表示式,這意味著我們要先完成 yield* 表示式指定的 Generator 函式,再繼續執行當前 Generator 函式。
  3. 你可以假設上面的程式碼被替換為如下程式碼:
function* g2() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
}
複製程式碼

Generator 會按這個順序執行結束。不過對於 yield* 和 return 的同時使用,我們需要特別注意,下一節將會提到。

Yield* 與 Return

帶 return 的 yield* 與一般 yield* 有點不同。當 yield* 與 return 語句一起使用時,yield* 被賦 return 的值,也就是整個 yield* function() 與其關聯 Generator 函式的返回值相等。

讓我們看看下面的程式碼和解釋,以便更好理解。

[譯] Javascript(ES6)Generator 入門

Yield* 與 Return

說明

  1. 第一個 next(),直接進入 yield 1 並返回其值。
  2. 第二個 next() 返回 2。
  3. 第三個 next(),執行 return 'foo' 後緊接著,yield 返回 'the end',其中 'foo' 被賦值到 const result
  4. 最後一個 next() 結束執行。

對內建 Iterable 物件使用 Yield*

yield* 還有一個值得一提的用法,它可以遍歷 iterable 物件,如 Array,String 和 Map。

一起看看實際執行結果。

[譯] Javascript(ES6)Generator 入門

對內建 Iterable 物件使用 Yield*

在程式碼中,yield* 遍歷傳入的每一個 iterable 物件,我覺得這段程式碼本身是不言自明的。

最佳實踐

最重要的是,每個 iterator/Generator 都可以使用 for…of 遍歷。與顯式呼叫的 next() 類似,for…of 迴圈依據 yield 關鍵字 進入下一次迭代。這裡是重點:它只會迭代到最後一個 yield,不會像 next() 那樣處理 return 語句。

下面的程式碼可以驗證以上描述。

[譯] Javascript(ES6)Generator 入門

Yield 與 for…of

最後 return 的值不會被列印,因為 for…of 迴圈只迭代到最後一個 yield。因此,作為最佳實踐,儘量避免在 Generator 函式中使用 return 語句,原因在於當使用 for…of 語句進行迭代時,return 會影響函式的可重用性。

[譯] Javascript(ES6)Generator 入門

總結

我希望這涵蓋了 Generator 函式的基本用法,希望這篇文章能讓你更好地理解 Generator 在 JavaScript 中的工作方式。如果你喜歡本文,請點個贊吧 :)。

請關注我的 GitHub 賬號獲取更多 JavaScript 和全棧專案:

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章