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