一個很簡短的 JS 生成器入門和用法參考

laobubu發表於2019-03-03

生成器函式

在寫其他 js 程式碼時會經常用到 debugger 的東西,能夠讓當前執行的函式暫停住。生成器函式裡的 yield 關鍵詞也能使得函式暫停執行,同時還能用來做資料的輸入輸出(如果是 debugger,只能手動去逐個 inspect 變數)。

yield 可以當作“斷點”。yield value 除了能當作斷點,還可以向外部輸出 value

此外,yield 表示式本身也是可以有值的,也就是說還可以表示外部傳入的資料。只要外部在呼叫 next(inputValue) 時輸入一個值。
這就是為什麼在賦值語句裡,可以用 yield 作為右值(例如,在使用了 react-saga 的專案裡到處都是這種玩意兒)。

function* fn1(){
  var data = yield "字串反轉器已啟動"
  yield data.split("").reverse().join("")
}

// 建立 Generator 物件
var g = fn1()

// 執行函式,直到遇到第一個“斷點”(yield 處)
console.log(g.next())

// 上次停頓的位置是 var data = yield "字串反轉器已啟動"
// 因此可以在繼續執行前,使得 (yield "字串反轉器已啟動") 的值為 "Hello"
console.log(g.next("Hello")) 

// 輸出:
// { value: `字串反轉器已啟動`, done: false }
// { value: `olleH`, done: false }
複製程式碼

Generator

正如前面的示例,呼叫一個 function* 生成器函式,會返回一個 Generator 物件(上文的 g)。這個物件是可迭代物件(最常見的用法,即用於 for of 迴圈中)。

此外,可以把它當作一個控制器,控制著一個被 yield 打了斷點(而且還沒開始執行)的函式。

Generator 其具備下列方法:

  1. next(inputValue?): { value, done }
    • (從頭,或者從上次停止的位置)開始執行生成器函式體,直到遇到 yield 或者 return
    • 注意:此函式可以有一個可選的引數 inputValue 。可以在繼續執行生成器函式前,為上次停頓所在的 yield 表示式設定一個值。
  2. throw(e): { value, done }
    • 如果生成器函式沒開始執行,則等同於原地 throw
    • 否則,在生成器函式體當前 yield 停頓的位置 throw 一個異常,然後繼續執行,直到遇到 yieldreturn 或者未被捕獲的異常(見下文)
  3. return(value): {value, done}
    • 強行終止(即使生成器函式還沒執行完畢),並指定一個 value 作為返回值
  4. [Symbol.iterator]()
    • 用於迭代協議的。效果同直接呼叫 next()

yield*

yield 關鍵詞相比,多了一個星號。可以把 yield* another_iterable 當作以下程式碼語法糖:

for (let item of another_iterable) {
  yield item;
}
複製程式碼

也就是說,在生成器函式裡yield* 就是針對一個可迭代物件,把它的每一項逐個地 yield 出來。

有趣的例子:

  • yield* [1,2,3] 會把3個數字逐個 yield 出來。陣列是可迭代的。
  • yield* "abcdefg" 會把這7個字母逐個 yield 出來。字串也是可迭代物件。
  • yield* another_generator 相當於把另一個 Generator 的輸出,當作自己的輸出給一個個 yield 出去了。
    • 這個有趣的特性可以用來做攔截器之類的應用。
    • 說到了串聯,如果是當前生成器函式想要利用另外一個生成器的返回值,直接呼叫 another_generator.next().value 就行了

生成器函式內的異常

next() 或者 throw(e) 可以讓生成器函式開始執行。在執行過程中,如果生成器函式裡遇到了未被捕獲的異常(可以是生成器內部自己產生的,或者由外部呼叫 throw(e) 塞進去的),那麼會在外部由 next() 或者 throw(e) 給 throw 出來

function* test(){
  try {
    console.log("inner: Hello")
    console.log("inner: GET" + (yield "output1"))
  } catch (err) {
    console.log("inner: Caught", err)
  }
  console.log("inner: Done")
  return "output 2"
}

var g = test()
console.log("outer: next: ", g.next("input 1"))
console.log("outer: throw: ", g.throw("err"))
console.log("outer: next: ", g.next("input 2"))
複製程式碼

上面的例子輸出如下。注意到由外部提供的 input1 不會被輸出,因為那是生成器函式還沒開始執行的時候傳進去的,能傳到哪裡?沒有任何的意義。

inner: Hello
outer: next:  { value: `output1`, done: false }
inner: Caught err
inner: Done
outer: throw:  { value: `output 2`, done: true }
outer: next:  { value: undefined, done: true }
複製程式碼

安利

寫了一個 Markdown 編輯器元件,只要一個框,所見即所得,而且還全面相容 CodeMirror(一個很強大的程式碼編輯元件)

悄悄安利一下: laobubu.net/HyperMD/

(歡迎來 GitHub 點贊 或者幫我買杯咖啡

相關文章