- 原文地址:A Simple Guide to Understanding Javascript (ES6) Generators
- 原文作者:Rajesh Babu
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:ssshooter
- 校對者:Zheng7426 hopsken
如果你在過去兩到五年中一直在研究 JavaScript,那麼肯定看過關於 Generator 和 Iterator 的文章。雖然 Generator 和 Iterator 本質上是相關的,但 Generator 似乎比 Iterator 更令人難以理解。
![[譯] Javascript(ES6)Generator 入門](https://i.iter01.com/images/6d8c09192af08ea754e45023b5060d91633b2da9c68baedd795f0670ca21e5b7.jpg)
Iterator 由 Iterable 物件(如 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 入門](https://i.iter01.com/images/09aca84c54b44374485f8948b1bf7497dd861dea89e301de52a65782074941ed.png)
介紹
眾所周知,JavaScript 的函式都會一直執行到 return 或函式結束。但對於 Generator 函式,會一直執行到 遇到 yield 或 return 或函式結束。與一般函式不同,Generator 函式一旦被呼叫,就會返回一個 Generator 物件。這個物件擁有 Generator Iterable,可以使用 next() 方法或 for…of 迴圈迭代它。
Generator 每次呼叫 next(),函式會一直執行到下一個 yield,然後暫停執行。
語法上他們的標誌是一個星號 *,function* X 和 function *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 入門](https://i.iter01.com/images/0cf77049c1bfcee3d82698ea8151a118c525ea4e91cf48f34f79cc0cdcddcfe4.png)
Generator 函式基礎
**注意:**在上面的例子中,直接訪問 Generator 函式總是執行到第一個 yield。因此,你需要將 Generator 分配給變數才能正確迭代它。
Generator 函式的生命週期
在深入理解之前,讓我們快速瀏覽一下 Generator 函式的生命週期示意圖:
![[譯] Javascript(ES6)Generator 入門](https://i.iter01.com/images/74dad3de4483bae96adabcf226a1a352de2eeda292694e0126aec18b209ad98f.png)
Generator 函式的生命週期
每次執行到 yield,Generator 函式都會返回一個物件,該物件包含 yield 產生的值和當前 Generator 函式的狀態。類似地,執行到 return,可以得到 return 的值,並且 done 的狀態為 true。當 done 的狀態為 true 時,意味著 Generator 函式已經執行完畢,後面的 yield 統統無效。
return 後的一切程式碼都會被忽略,包括 yield 表示式。
繼續閱讀深入理解上圖。
把 yield 賦值到一個變數
在的示例中,我們建立了一個帶有 yield 的最基本的 Generator,並獲得了預期的輸出。在下面程式碼中,我們將整個 yield 表示式賦值到一個變數。
![[譯] Javascript(ES6)Generator 入門](https://i.iter01.com/images/f48408799f4fe300fdd26ce4d1ea87507a026973c3882da7bae59698e1b714e2.png)
把 yield 賦值到一個變數
把整個 yield 表示式傳到變數的結果是什麼?Undefined …
為什麼會是 undefined?從第二個 next() 開始,前一個 yield 會被替換為 next 函式的引數。因為例子中的 next 沒有傳入任何值,所以程式判定“前一個 yield 表示式”為 undefined。
這是重點中的重點,下面的章節我們將詳細介紹對 next() 傳參的用法。
將引數傳遞給 next() 方法
參考上面的示意圖,我們聊聊關於傳參到 next 函式的事情。這是整個 Generator 使用中最棘手的部分之一。
思考以下程式碼,其中 yield 被賦給變數,但這次我們向 next() 傳參。
看看控制檯的輸出,先思考一下,後面會有解釋。
![[譯] Javascript(ES6)Generator 入門](https://i.iter01.com/images/dd4d64fa869ab8165002c8d83ecfa962e19a55a7a75d053cece1b80aa252059b.png)
將引數傳遞給 next()
說明:
- 在呼叫 next(20) 的時候,第一個 yield 前的程式碼都被執行。因為前面已經沒有 yield,傳入的 20 毫無作用。輸出 yield 的 value 為 i*10,也就是 100。因為執行到第一個 yield 停止,所以 const j 未被賦值。
- 呼叫 next(10) 時,第一個 yield 的位置被替換為 10,相當於在返回第二個 yield 的 value 前,設定 yield (i * 10) = 10,所以 j 為 50。yield 的 value 為 2 * 50 / 4 = 25。
- 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 入門](https://i.iter01.com/images/d698e5b11243a05797085db346a0206e679f8ff1d46368a0e00c68e5488d7aae.png)
Yield 作為其他函式的引數
解釋
- 第一個 next() yield(生成) 的 value 為 undefined,因為 yield 表示式無值。
- 第二個 next() 生成的 value 為被傳入的
'I am usless'
,這一步為函式呼叫準備了引數。 - 第二個 next() 以 undefined 為引數呼叫了後面的函式。next() 沒有接收引數,意味著上一個 yield 表示式的值為 undefined,所以函式列印出 undefined 並終止執行。
對函式呼叫使用 yield
除了返回普通的值,yield 還可以呼叫函式並返回他的值。看看下面的例子更好理解:
![[譯] Javascript(ES6)Generator 入門](https://i.iter01.com/images/b95e251e4b73ce3d53f9dce4829e7a5c4e891f47a8e0b77266bef574b0fe9076.png)
對函式呼叫使用 yield
上述程式碼返回了函式返回的物件作為 yield 的 value,然後把 const user 賦值為 undefined,結束執行。
對 Promise 使用 yield
對 promise 使用 yield 與對函式呼叫使用 yield 相似,它會返回一個 promise,我們以此進一步判定操作成功或失敗。看看以下程式碼,瞭解它的使用方法:
![[譯] Javascript(ES6)Generator 入門](https://i.iter01.com/images/a154c41e498d9bd9bdf01db40eaed020a416a6a14990aa8c2ad0840f9e4f2bd5.png)
對 Promise 使用 yield
apiCall 將 promise 作為 yield value 返回,在 2 秒後 resolved 並列印出我們需要的值。
Yield*
Yield 表示式的介紹就告一段落了,接著我們瞭解一下另一個表示式 yield*
。Yield*
在 Generator 函式中使用時,會把迭代委託到下一個 Generator 函式。簡單來說,會先同步完成 Yield*
表示式中的 Generator 函式,再繼續執行外層函式。
讓我們看看下面的程式碼和解釋,以便更好地理解。此程式碼來自 MDN Web 文件。
![[譯] Javascript(ES6)Generator 入門](https://i.iter01.com/images/2dab2a323fc654d3b943551ea7caa278f219af2844ca97e2b00eadb00eeef928.png)
Yield*
基礎
解釋
- 呼叫第一個 next(),產生的值為1。
- 第二個 next() 呼叫的是
yield*
表示式,這意味著我們要先完成yield*
表示式指定的 Generator 函式,再繼續執行當前 Generator 函式。 - 你可以假設上面的程式碼被替換為如下程式碼:
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 入門](https://i.iter01.com/images/05087bd26723fad77af7b6f60e03677c96c2b4275d89b51bd2a9ab0c0b7abe79.png)
Yield*
與 Return
說明
- 第一個 next(),直接進入 yield 1 並返回其值。
- 第二個 next() 返回 2。
- 第三個 next(),執行 return 'foo' 後緊接著,yield 返回 'the end',其中 'foo' 被賦值到 const result。
- 最後一個 next() 結束執行。
對內建 Iterable 物件使用 Yield*
yield*
還有一個值得一提的用法,它可以遍歷 iterable 物件,如 Array,String 和 Map。
一起看看實際執行結果。
![[譯] Javascript(ES6)Generator 入門](https://i.iter01.com/images/3dd6b42a27a7db5fb11de98ffdd483cfaa267741feb5ce3ff79005994fa473d6.png)
對內建 Iterable 物件使用 Yield*
在程式碼中,yield*
遍歷傳入的每一個 iterable 物件,我覺得這段程式碼本身是不言自明的。
最佳實踐
最重要的是,每個 iterator/Generator 都可以使用 for…of 遍歷。與顯式呼叫的 next() 類似,for…of 迴圈依據 yield 關鍵字 進入下一次迭代。這裡是重點:它只會迭代到最後一個 yield,不會像 next() 那樣處理 return 語句。
下面的程式碼可以驗證以上描述。
![[譯] Javascript(ES6)Generator 入門](https://i.iter01.com/images/2d4e4f2c5cb155c08b4c1e5afdaca8bfe31db418ed3aabdae8e79a5806554708.png)
Yield 與 for…of
最後 return 的值不會被列印,因為 for…of 迴圈只迭代到最後一個 yield。因此,作為最佳實踐,儘量避免在 Generator 函式中使用 return 語句,原因在於當使用 for…of 語句進行迭代時,return 會影響函式的可重用性。
![[譯] Javascript(ES6)Generator 入門](https://i.iter01.com/images/74ae433578b84305304aa6997943f283df6c917e00345c6f1a6087dae52a15aa.jpg)
總結
我希望這涵蓋了 Generator 函式的基本用法,希望這篇文章能讓你更好地理解 Generator 在 JavaScript 中的工作方式。如果你喜歡本文,請點個贊吧 :)。
請關注我的 GitHub 賬號獲取更多 JavaScript 和全棧專案:
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。