原文鏈接:https://medium.freecodecamp.org/lets-explore-es6-generators-5e58ed23b0f1
作者:Tiago Lopes Ferreia
譯者:Soyaine

生成器(Generators)是可迭代協(xié)議(iterables)的一種實現(xiàn)。
它的最大特點在于,它是可以在保持上下文狀態(tài)的情況下暫停執(zhí)行的函數(shù),當(dāng)處理需要暫停的執(zhí)行邏輯,但在恢復(fù)執(zhí)行時需要之前的上下文時,這就起到了很重要的作用??吹竭@里有沒有想到了異步編程?
語法
生成器的語法以 function* 作為開頭的標(biāo)識符(注意末尾的星號),同時穿插著 yield 來實現(xiàn)執(zhí)行的暫停。下面以具體的代碼演示這個過程。
function* generator() {
// 代碼A
yield 'foo'
// 代碼B
}
調(diào)用上面的 generator 函數(shù)時,將會創(chuàng)建一個新的生成器,我們可以通過 next 函數(shù)來用它控制程序邏輯。運行 next 將會執(zhí)行這個生成器中代碼,直到 yield 的語句。到這個點時,yield 語句中的 value 的值將被返回,同時 generator 的執(zhí)行也被掛起。
const g = generator()
g.next() // { value: 'foo', done: false }
// 生成器中的代碼A 被執(zhí)行,
// value 中的 'foo' 通過 yield 被返回,
// 然后,生成器的執(zhí)行被掛起。
g.next() // { value: undefined, done: fales }
// 在這個階段,剩下的代碼(如代碼B)繼續(xù)被執(zhí)行,
// 由于沒有返回值,所以我得到 'undefined' 作為 value 的值,
// 同時這個迭代器(iterator)返回 true 以表示迭代結(jié)束。
yield
yield 伴隨著生成器出現(xiàn),并且允許我們在執(zhí)行的中途返回值,但只能在生成器內(nèi)部這樣做。如果我們試圖在回調(diào)時返回一個值的話,就算在生成器中聲明了,也會報錯。
function* generator() {
['foo', 'bar'].forEach(e => yield e) // SyntaxError 語法錯誤
// 我們不能在一個非 generator 的函數(shù)中使用 yield
//(譯者注:此例子從 yield 被寫在了 forEach 函數(shù)中)
}
yield*
yield* 是用來在一個生成器中調(diào)用另一個生成器的。
function* foo() {
yield 'foo'
}
// 我們?nèi)绾卧?bar 生成器中調(diào)用 foo 生成器呢?
function* bar() {
yield 'bar'
foo()
yield 'bar again'
}
const b = bar();
b.next() // { value: 'bar', done: false }
b.next() // { value: 'bar again', done: false }
b.next() // { value: undefined, done: true }
這里的 b 迭代器由 bar 生成器產(chǎn)生,但在調(diào)用 ‘foo’ 的時候并不會按照期待執(zhí)行。這是因為,盡管運行 ‘foo’ 時產(chǎn)生了一個迭代器,但我們并不會迭代它。這就是 ES6 會帶來 yield* 操作符的原因。
function* foo() {
yield 'foo'
}
function* bar() {
yield 'bar'
yield* foo()
yield 'bar again'
}
const b = bar();
b.next() // { value: 'bar', done: false }
b.next() // { value: 'foo', done: false }
b.next() // { value: 'bar again', done: false }
b.next() // { value: undefined, done: true }
這對數(shù)據(jù)迭代也完美適用:
function* foo() {
yield 'foo'
}
function* bar() {
yield 'bar'
yield* foo()
yield 'bar again'
}
for (let e of bar()) {
console.log(e)
// bar
// foo
// bar again
}
console.log([...bar()]) // [ 'bar', 'foo', 'bar again' ]
在 yield* 中遍歷這個生成器中的所有元素,然后 yield 返回這個值。
function* bar() {
yield 'bar'
for (let e of foo()) {
yield e
}
yield 'bar again'
}
作為迭代器(Iterators)的生成器(Generators)

之所以說生成器是簡單的迭代器,是因為它們同樣遵循了可迭代協(xié)議(iterable protocol)和迭代器協(xié)議(iterator protocol):
- 可迭代協(xié)議指出,一個可迭代的對象應(yīng)該返回一個含有 key 為
Symbol.iterator的迭代器。const g = generator() typeof g[Symbol.iterator] // function - 迭代器協(xié)議指出,迭代器對象應(yīng)該是一個指向迭代函數(shù)(iteration)中下一個元素的對象,并且這個對象包含一個叫
next的函數(shù)。const iterator = g[Symbol.iterator]() typeof iterator.next // function - 由于生成器是可迭代對象,所以我們可以使用遍歷方法來迭代循環(huán)其中的值,例如
for-of。for (let e of iterator) { console.log(e) // 'foo' }
Return
我們可以在生成器中添加一個 return 語句,但由于生成器的數(shù)據(jù)處理規(guī)則是迭代,所以 return 的作用與平常有所區(qū)別。
function* generatorWithReturn() {
yield 'foo'
yield 'bar'
return 'done'
}
var g = generatorWithReturn()
g.next() // { value: 'foo', done: false }
g.next() // { value: 'bar', done: false }
g.next() // { value: 'done', done: true }
當(dāng)手動執(zhí)行這個迭代函數(shù)的時候,使用 next,我們得到的迭代器的最后一個 value 是 return 的返回值(如 done),此時 done 的標(biāo)記值為 true。
但是在使用一個像 for-of 或者 destructuring 這樣定義好的數(shù)據(jù)遍歷操作時,返回值會被忽略。
for (let e of g) {
console.log(e)
// 'foo'
// 'bar'
}
console.log([...g]) // ['foo', 'bar']
yield*
我們知道 yield* 允許我們在一個生成器中調(diào)用另一個生成器,同時它也允許我們通過已執(zhí)行的生成器來存儲返回值。
function* foo() {
yield 'foo'
return 'foo done'
}
function* bar() {
yield 'bar'
const result = yield* foo()
yield result
}
for (let e of bar()) {
console.log(e)
// bar
// foo
// foo done
}
Throw
我們可以在生成器內(nèi)部使用 throw,next 會傳遞發(fā)生的異常。一旦拋出異常,迭代器的流程將會中斷,狀態(tài)被設(shè)置為 done: true,并無限保持。
function* generatorWithThrow() {
yield 'foo'
throw new Error('Ups!')
yield 'bar'
}
var g = generatorWithReturn()
g.next() // { value: 'foo', dxone: false }
g.next() // Error: Ups!
g.next() // { value: undefined, done: true }
用于數(shù)據(jù)遍歷的生成器
生成器除了因 yield 而有了數(shù)據(jù)生成的作用,使用 next 它也可以遍歷數(shù)據(jù)。
function* generatorDataConsumer() {
// A
console.log('Ready to consume!')
while (true) {
const input = yield; // B
console.log(`Got: ${input}`)
}
}
這里也有一些有趣的點可以探索。(請看代碼及之后的解釋)
// (1)
var g = generatorDataConsumer()
// (2)
g.next() // { value: undefined, done: false }
// Ready to consume!
// (3)
g.next('foo') // { value: undefined, done: false }
// Got: foo
創(chuàng)建生成器 (1)
在這一步創(chuàng)建了一個 g 生成器,程序會在 A 處停止執(zhí)行。
第一個 next (2)
執(zhí)行第一個 next 時,生成器會執(zhí)行到第一個 yield 語句。第一次執(zhí)行的時候,通過 next 傳遞的任何值都會被忽略,這是因為沒有出現(xiàn) yield 語句。所以執(zhí)行過程會在 B 處暫停,等待 yield 被賦值。
下一個 next (3)
在下一次執(zhí)行 next 時,生成器會運行代碼至下一個 yield。在我們上面的例子中,它打印了通過 yield 得到的值(i.e. Got: foo),并且再次在 yield 處暫停。
應(yīng)用例子

實現(xiàn)可迭代協(xié)議
因為生成器是可迭代協(xié)議的一種實現(xiàn),所以當(dāng)它被創(chuàng)建的時候我們也就得到了一個可迭代的對象,在這個對象中,yield 代表了每次迭代時傳遞出的值。這使得我們可以用生成器來創(chuàng)建迭代器。
下面這個例子展示了一個可迭代的生成器遍歷數(shù)據(jù)并找出最大值的過程。因為我們的生成器會返回一個可迭代對象,所以我們可以用 for-of 來遍歷這些值。
記住 yield 會暫停生成器的執(zhí)行過程,每次迭代時,生成器會從之前暫停的地方重新開始。
function* evenNumbersUntil() {
for (let value = 0; value <= max; value += 2) {
// 當(dāng) 'value' 的值是偶數(shù)時,我們把它 'yield'
// 并作為迭代過程中的下一個值
if (value % 2 === 0) yield value;
}
}
// 現(xiàn)在我們可以用 'for-of' 來迭代這些值了
for (let e of evenNumbersUntil(10)) {
console.log(e)
// 0
// 2
// 4
// 6
// 8
// 10
}
異步的代碼
我們可以用生成器來更好實現(xiàn)異步代碼,比如 promises。這部分的例子很好的介紹了 ES8 中的新特性 async/await 。
下面要展示一個用 promises 來獲取 JSON 文件的例子,我們將用 Jake Archibald 的在 developers.google.com 上的例子。
為了讓我們的代碼看起來更像同步的風(fēng)格,我們使用了 co 庫。至于 async/await 的話,我們的代碼會有點像我們之前的版本。
結(jié)尾
這是 Axel Rauschmayer 在 Exploring ES6 上的圖,向我們展示了生成器是如何與迭代器聯(lián)系在一起的。

生成器是可迭代協(xié)議的一種實現(xiàn),且遵循可迭代協(xié)議和迭代器協(xié)議,因此它們都可以用于建立可迭代對象。
生成器最令人驚奇的點在于它可以在執(zhí)行過程中暫停,為此 ES6 引入了 yield 的新特性。
然而在生成器中調(diào)用生成器并非像執(zhí)行生成器函數(shù)那么簡單,為了解決這個問題,ES6 中又有了 yield*。
生成器為讓異步編程向同步風(fēng)格的發(fā)展更近了一步。
致謝
- Axel Rauschmayer 的 Exploring ES6?—?Generators
- Nicolás Bevacqua 的 PonyFoo?—?ES6 Generators in Depth
- Jake Archibald 放在 developers.google.com 的 promise 的代碼例子
- 感謝所有 Regular Show 的粉絲