《javascript高級程序設計》學習筆記 | 7.3.生成器

關注前端小謳,閱讀更多原創技術文章

生成器

  • ES6 新增的結構,能夠在一個函數塊內暫停和恢復代碼執行,能夠自定義迭代器實現協程

相關代碼 →javascript

生成器基礎

  • 生成器的形式是一個函數,函數名稱前加一個星號*
  • 能夠定義函數的地方,均可以定義生成器(箭頭函數除外
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狀態不一樣java

    • 經過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關鍵字還能夠做爲函數的中間參數使用git

    • 上一次讓生成器函數暫停的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關鍵字同時使用)github

    • 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) {
    // 第二、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:truevalue的屬性:算法

    • 對於普通迭代器,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 關鍵字,分別用代碼實現如下功能:this

    • 迭代 5 次,獲取每次迭代值和索引
    • 獲取大於 3 小於 9 的整數
    • 從 1 開始,填充長度爲 6 的自增數組
    • 求出斐波那契數列第 20 項數字的值(從 0 算起)
  • yield*的做用是什麼?在普通迭代器、生成器函數產生的迭代器中,yield*的值分別是什麼?
  • 如何將生成器做爲默認迭代器?return()和 throw()方法提早終止生成器的用法分別是什麼?
相關文章
相關標籤/搜索