關注前端小謳,閱讀更多原創技術文章
生成器
- ES6 新增的結構,可以在一個函式塊內暫停和恢復程式碼執行,可以自定義迭代器和實現協程
相關程式碼 →
生成器基礎
- 生成器的形式是一個函式,函式名稱前加一個星號
*
- 可以定義函式的地方,都可以定義生成器(箭頭函式除外)
function* generatorFn() {} // 生成器函式宣告
let gfn = function* () {} // 生成器函式表示式
let foo = {
*generatorFn() {}, // 生成器函式作為物件字面量方法
}
class Foo {
*generatorFn() {} // 生成器函式作為類例項方法
}
class FooStatic {
static *generatorFn() {} // 生成器函式作為類靜態方法
}
- 呼叫生成器函式會產生一個生成器物件,生成器物件實現了 Iterator 介面,具有
next()
方法
const g = generatorFn() // 呼叫生成器函式,產生生成器物件
console.log(g) // generatorFn {<suspended>},生成器物件
console.log(g.next) // 生成器物件具有next()方法
next()
方法的返回值類似於迭代器,有done 屬性和value 屬性- 函式體為空的生成器呼叫一次
next()
就會達到done:true
狀態
console.log(g.next()) // { value: undefined, done: true },函式體為空
- 可以透過生成器函式的返回值指定
value
的返回值(預設為 undefined)
function* generatorFn2() {
return 'foo'
}
const g2 = generatorFn2()
console.log(g2.next()) // { value: 'foo', done: true }
console.log(g2.next()) // { value: undefined, done: true },耗盡
- 生成器函式只會在初次呼叫
next()
方法後開始執行
function* generatorFn3() {
console.log('生成器函式開始執行')
}
const g3 = generatorFn3() // 呼叫生成器函式,產生生成器物件(生成器函式還未執行,不列印日誌)
g3.next() // '生成器函式開始執行',初次呼叫next()方法,生成器函式開始執行,列印日誌
- 生成器物件實現了 Iterable 介面,其預設迭代器是自引用的
function* generatorFn4() {}
console.log(generatorFn4) // ƒ* generatorFn4() {},生成器函式
const g4 = generatorFn4()
console.log(g4) // generatorFn4 {<suspended>},生成器物件
console.log(g4[Symbol.iterator]) // ƒ [Symbol.iterator]() { [native code] },迭代器工廠函式
console.log(g4[Symbol.iterator]()) // generatorFn4 {<suspended>},迭代器
console.log(g4 === g4[Symbol.iterator]()) // true,生成器物件的預設迭代器是自引用的
透過 yield 中斷執行
yield
關鍵字可以讓生成器停止和開始執行:- 生成器遇到
yield
關鍵字之前正常執行 - 遇到
yield
關鍵字後停止執行,函式作用域的狀態被保留 - 停止執行的生成器函式透過生成器物件呼叫
next()
方法恢復執行
- 生成器遇到
function* generatorFn5() {
yield
}
let g5 = generatorFn5()
console.log(g5.next()) // { value: undefined, done: false },yield生成的值
console.log(g5.next()) // { value: undefined, done: true },恢復執行生成的值
yield
關鍵字與函式的return
語句作用相似,其生成的值會出現在next()
方法返回的物件裡,但done
的狀態不同:- 透過
yield
關鍵字退出的生成器函式會處在done:false
狀態 - 透過
return
關鍵字退出的生成器函式會處在done:true
狀態
- 透過
function* generatorFn6() {
yield 'foo'
yield 'bar'
return 'baz'
}
let g6 = generatorFn6()
console.log(g6.next()) // { value: 'foo', done: false },yield關鍵字退出
console.log(g6.next()) // { value: 'bar', done: false },yield關鍵字退出
console.log(g6.next()) // { value: 'baz', done: true },return關鍵字退出
- 同一個生成器函式的不同生成器物件之間沒有聯絡,一個生成器物件上呼叫
next()
方法不影響其他生成器
let g7 = generatorFn6() // 生成器物件g7
let g8 = generatorFn6() // 生成器物件g8
console.log(g7.next()) // { value: 'foo', done: false }
console.log(g8.next()) // { value: 'foo', done: false }
console.log(g8.next()) // { value: 'bar', done: false }
console.log(g7.next()) // { value: 'bar', done: false }
yield
關鍵字必須在生成器函式內部,直接位於生成器函式定義中使用,用在其他地方或巢狀的非生成器函式會報錯
function* validGeneratorFn() {
yield 'result'
}
function* invalidGeneratorFnA() {
function a() {
yield 'result' // SyntaxError: Unexpected string
}
}
function* invalidGeneratorFnB() {
const b = () => {
yield 'result' // SyntaxError: Unexpected string
}
}
function* invalidGeneratorFnC() {
;(() => {
yield 'result' // SyntaxError: Unexpected string
})()
}
生成器物件作為可迭代物件
- 把生成物件當成可迭代物件
function* generatorFn7() {
// 生成器函式
yield 1
yield 2
yield 3
}
for (const x of generatorFn7()) {
// 呼叫生成器函式generatorFn7,generatorFn7()是生成器物件
console.log(x)
/*
1
2
3
*/
}
- 使用生成器控制迭代迴圈的次數
function* nTimes(n) {
while (n--) {
console.log(n)
yield
}
}
for (let _ of nTimes(3)) {
console.log(_)
/*
2,第1次迴圈n
undefined,第1次迴圈yield
1,第2次迴圈n
undefined,第2次迴圈yield
0,第3次迴圈n
undefined,第3次迴圈yield
*/
}
使用 yield 實現輸入和輸出
除了作為函式的中間返回語句使用,
yield
關鍵字還可以作為函式的中間引數使用- 上一次讓生成器函式暫停的
yield
關鍵字會接收到傳給next()
方法的第一個值 - 第一次呼叫
next()
傳入的值不會被使用,因為僅僅是為了開始執行生成器函式
- 上一次讓生成器函式暫停的
function* generatorFn8() {
console.log(yield)
console.log(yield)
console.log(yield)
}
let g9 = generatorFn8() // 呼叫生成器函式,產生生成器物件
g9.next('bar') // 第一次呼叫next()的值不會被使用,僅作為開始執行生成器函式
g9.next('baz') // 'baz',呼叫next()傳入baz,引數作為交給同一個yield的值
g9.next('qux') // 'qux',呼叫next()傳入qux,引數作為交給同一個yield的值
yield
關鍵字同時用於輸入和輸出(與return
關鍵字同時使用)next()
方法沒有引數,生成器函式直到遇到yield
關鍵字停止執行next()
方法有引數,引數作為交給同一個 yield 的值,生成器函式執行return
返回本次傳入的值
function* generatorFn9() {
return yield 'foo'
}
let g10 = generatorFn9()
console.log(g10.next()) // { value: 'foo', done: false },next()沒有引數,遇到yield關鍵字暫停執行,並計算要產生的值
console.log(g10.next('bar')) // { value: 'bar', done: true },next()有引數,引數作為交給同一個yield的值,相當於return 'bar'
yield
關鍵字多次使用
function* generatorFn10() {
for (let i = 0; ; i++) {
yield i
}
}
let g11 = generatorFn10()
console.log(g11.next()) // { value: 0, done: false }
console.log(g11.next()) // { value: 1, done: false }
console.log(g11.next()) // { value: 2, done: false }
console.log(g11.next()) // { value: 3, done: false }
- 根據迭代次數產生相應索引
function* nTimes(n) {
let i = 0
while (n--) {
yield i++
}
}
for (let x of nTimes(3)) {
console.log(x)
/*
0
1
2
*/
}
- 使用生成器實現範圍
function* range(start, end) {
while (end > start) {
yield start++
}
}
for (const x of range(4, 7)) {
console.log(x)
/*
4
5
6
*/
}
- 使用生成器填充陣列
function* zeros(n) {
while (n--) {
yield 0
}
}
console.log(zeros(8)) // zeros {<suspended>},生成器物件
console.log(Array.from(zeros(8))) // [0, 0, 0, 0, 0, 0, 0, 0],生成器物件作為可迭代物件
- 使用生成器實現斐波那契數列
function* fibonacci() {
let arr = [0, 1]
let [prev, curr] = arr
while (true) {
;[prev, curr] = [curr, prev + curr]
arr.push(curr)
yield arr
}
}
function Fibonacci(n) {
if (n === 1) {
// 第1項
return 0
} else if (n === 2 || n === 3) {
// 第2、3項
return 1
} else {
// 第4項或之後
let num = 0
const fibo = fibonacci()
for (let i = 3; i <= n; i++) {
num = fibo.next().value
}
return num
}
}
console.log(Fibonacci(8).join()) // 0,1,1,2,3,5,8,13
產生可迭代物件
- 用星號
*
增強yield
,讓其能夠迭代一個可迭代物件,yield*
將一個可迭代物件序列化為一連串單獨產出的值
function* generatorFn11() {
yield* [1, 2, 3]
}
let g12 = generatorFn11()
for (const x of generatorFn11()) {
console.log(x)
/*
1
2
3
*/
}
// 等價於
function* generatorFn11() {
for (const x of [1, 2, 3]) {
yield x
}
}
yield*
的值是關聯迭代器返回done:true
時value
的屬性:- 對於普通迭代器,
done:true
代表迭代器耗盡,這個值是undefined
function* generatorFn12() { console.log('iterValue', yield* [1, 2, 3]) } for (const x of generatorFn12()) { console.log('value', x) /* value 1 value 2 value 3 iterValue undefined */ }
- 對於普通迭代器,
- 對於生成器函式產生的迭代器,
done:true
的值是return 返回的值(沒有 return 值則返回 undefined)
function* innerGeneratorFn() {
yield 'foo'
return 'bar'
}
function* outerGeneratorFn() {
console.log('iterValue', yield* innerGeneratorFn())
}
for (const x of outerGeneratorFn()) {
console.log('value', x)
/*
value foo
iterValue bar
*/
}
使用 yield*
實現遞迴演算法
- 用
yield*
實現遞迴,此時生成器可以產生自身
function* nTimes(n) {
if (n > 0) {
yield* nTimes(n - 1) // 生成器物件作為可迭代物件
yield n
}
}
for (const x of nTimes(3)) {
console.log(x)
/*
1
2
3
*/
}
生成器作為預設迭代器
- 生成器物件實現了
Iterable
介面,生成器函式和預設迭代器被呼叫之後都產生迭代器
class Foo2 {
// Foo既是迭代器,又是生成器函式
constructor() {
this.values = [1, 2, 3]
}
*[Symbol.iterator]() {
yield* this.values
}
}
const f = new Foo2() // 產生可迭代的生成器物件
for (const x of f) {
console.log(x)
/*
1
2
3
*/
}
提前終止生成器
- 一個實現
Iterator
介面的物件一定有next()
方法,還有一個可選的return()
方法,生成器還有第三個方法throw()
return()
和throw()
都可以用於強制生成器進入關閉狀態
function* generatorFn13() {}
let g13 = generatorFn13() // 生成器物件
console.log(g13.next) // ƒ next() { [native code] }
console.log(g13.return) // ƒ return() { [native code] }
console.log(g13.throw) // ƒ throw() { [native code] }
return
return()
方法返回種種迭代器物件的值(即 return()方法的引數)
function* generatorFn14() {
yield* [1, 2, 3]
}
let g14 = generatorFn14()
console.log(g14) // generatorFn14 {<suspended>}
console.log(g14.return(5)) // {value: 5, done: true}
console.log(g14) // generatorFn14 {<closed>}
- 透過
return()
方法進入關閉狀態的生成器物件,後續呼叫next()
都會顯示done:true
狀態,後續提供的任何返回值都不再被儲存或傳播
console.log(g14.next()) // { value: undefined, done: true },已經呼叫過return()
console.log(g14.next()) // { value: undefined, done: true }
console.log(g14.next()) // { value: undefined, done: true }
for-of
等內建語言結構會忽略狀態為done:true
的迭代器物件內部返回的值(忽略 undefined)
let g15 = generatorFn14()
for (const x of g15) {
x > 1 && g15.return() // x大於1則停止生成器
console.log(x)
/*
1
2
自動忽略done:true返回的value(undefined)
*/
}
throw
throw()
方法會在暫停的時候,將一個提供的錯誤注入到生成器物件中- 如果錯誤未被處理,則生成器關閉
function* generatorFn15() { yield* [1, 2, 3] } let g16 = generatorFn15() console.log(g16) // generatorFn15 {<suspended>} try { g16.throw('foo') // 注入錯誤 } catch (err) { console.log(err) // 'foo' } console.log(g16) // generatorFn15 {<closed>},錯誤未被處理,生成器關閉
- 如果錯誤在生成器函式內部處理,則生成器不會關閉,且可以恢復執行;錯誤處理會跳過對應的
yield
function* generatorFn16() {
for (const x of [1, 2, 3]) {
// 錯誤在生成器的try/catch塊中丟擲 -> (生成器物件已開始執行)在生成器內部被捕獲
// 若生成器物件未開始執行,則throw()丟擲的錯誤不會在生成器內部被捕獲
try {
yield x // 在yield關鍵字處暫停執行
} catch (err) {
console.log(err) // 'foo'
}
}
}
let g17 = generatorFn16()
console.log(g17.next()) // { value: 1, done: false }
g17.throw('foo') // 注入錯誤
console.log(g17.next()) // { value: 3, done: false },跳過對應的yield
總結 & 問點
- 什麼是生成器?哪些函式可以定義生成器?
- 如何獲取生成器物件?如何指定生成器 next()方法的 value 返回值?生成器函式什麼時候開始執行?
- 如何理解“生成器物件的預設迭代器是自引用”的?
- yield 關鍵字在生成器中的作用是什麼?其和 return 關鍵字的返回值有什麼不同
- 同一個生成器方法生成的不同生成器物件之間有聯絡麼?
請使用生成器函式和 yield 關鍵字,分別用程式碼實現以下功能:
- 迭代 5 次,獲取每次迭代值和索引
- 獲取大於 3 小於 9 的整數
- 從 1 開始,填充長度為 6 的自增陣列
- 求出斐波那契數列第 20 項數字的值(從 0 算起)
yield*
的作用是什麼?在普通迭代器、生成器函式產生的迭代器中,yield*
的值分別是什麼?- 如何將生成器作為預設迭代器?return()和 throw()方法提前終止生成器的用法分別是什麼?