JavaScript中Generator函數yield表示式範例詳解

2022-11-01 18:01:06
本篇文章給大家帶來了關於的相關知識,其中主要為大家介紹了JS Generator函數yield表示式範例詳解,Generator函數是ES6提供的一種非同步程式設計解決方案,下面一起來看一下,希望對大家有幫助。

前端(vue)入門到精通課程:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:

【相關推薦:、】

什麼是 Generator 函數

在Javascript中,一個函數一旦開始執行,就會執行到最後或遇到return時結束,執行期間不會有其它程式碼能夠打斷它,也不能從外部再傳入值到函數體內

而Generator函數(生成器)的出現使得打破函數的完整執行成為了可能,其語法行為與傳統函數完全不同

Generator函數是ES6提供的一種非同步程式設計解決方案,形式上也是一個普通函數,但有幾個顯著的特徵:

  • function關鍵字與函數名之間有一個星號 "*" (推薦緊挨著function關鍵字)
  • 函數體內使用 yield 表示式,定義不同的內部狀態 (可以有多個yield)
  • 直接呼叫 Generator函數並不會執行,也不會返回執行結果,而是返回一個遍歷器物件(Iterator Object)
  • 依次呼叫遍歷器物件的next方法,遍歷 Generator函數內部的每一個狀態
  // 傳統函數
  function foo() {
    return 'hello world'
  }
  foo()   // 'hello world',一旦呼叫立即執行
  // Generator函數
  function* generator() {
    yield 'status one'         // yield 表示式是暫停執行的標記  
    return 'hello world'
  }
  let iterator = generator()   // 呼叫 Generator函數,函數並沒有執行,返回的是一個Iterator物件
  iterator.next()              // {value: "status one", done: false},value 表示返回值,done 表示遍歷還沒有結束
  iterator.next()              // {value: "hello world", done: true},value 表示返回值,done 表示遍歷結束
登入後複製

上面的程式碼中可以看到傳統函數和Generator函數的執行是完全不同的,傳統函數呼叫後立即執行並輸出了返回值;Generator函數則沒有執行而是返回一個Iterator物件,並通過呼叫Iterator物件的next方法來遍歷,函數體內的執行看起來更像是「被人踢一腳才動一下」的感覺

  function* gen() {
    yield 'hello'
    yield 'world'
    return 'ending'
  }
  let it = gen()
  it.next()   // {value: "hello", done: false}
  it.next()   // {value: "world", done: false}
  it.next()   // {value: "ending", done: true}
  it.next()   // {value: undefined, done: true}
登入後複製

上面程式碼中定義了一個 Generator函數,其中包含兩個 yield 表示式和一個 return 語句(即產生了三個狀態)

每次呼叫Iterator物件的next方法時,內部的指標就會從函數的頭部或上一次停下來的地方開始執行,直到遇到下一個 yield 表示式或return語句暫停。換句話說,Generator 函數是分段執行的,yield表示式是暫停執行的標記,而 next方法可以恢復執行

第四次呼叫next方法時,由於函數已經遍歷執行完畢,不再有其它狀態,因此返回 {value: undefined, done: true}。如果繼續呼叫next方法,返回的也都是這個值

yield 表示式

yield 表示式只能用在 Generator 函數裡面,用在其它地方都會報錯

function (){
    yield 1;
  })()
  // SyntaxError: Unexpected number
  // 在一個普通函數中使用yield表示式,結果產生一個句法錯誤
登入後複製

yield 表示式如果用在另一個表示式中,必須放在圓括號裡面

function* demo() {
    console.log('Hello' + yield); // SyntaxError
    console.log('Hello' + yield 123); // SyntaxError
    console.log('Hello' + (yield)); // OK
    console.log('Hello' + (yield 123)); // OK
  }
登入後複製

yield 表示式用作引數或放在賦值表示式的右邊,可以不加括號

function* demo() {
    foo(yield 'a', yield 'b'); // OK
    let input = yield; // OK
  }
登入後複製

yield 表示式和return語句的區別

相似:都能返回緊跟在語句後面的那個表示式的值

區別:

  • 每次遇到 yield,函數就暫停執行,下一次再從該位置繼續向後執行;而 return 語句不具備記憶位置的功能
  • 一個函數只能執行一次 return 語句,而在 Generator 函數中可以有任意多個 yield

yield* 表示式

如果在 Generator 函數裡面呼叫另一個 Generator 函數,預設情況下是沒有效果的

function* foo() {
    yield 'aaa'
    yield 'bbb'
  }
  function* bar() {
    foo()
    yield 'ccc'
    yield 'ddd'
  }
  let iterator = bar()
  for(let value of iterator) {
    console.log(value)
  }
  // ccc
  // ddd
登入後複製

上例中,使用 for...of 來遍歷函數bar的生成的遍歷器物件時,只返回了bar自身的兩個狀態值。此時,如果想要正確的在bar 裡呼叫foo,就需要用到 yield* 表示式

yield* 表示式用來在一個 Generator 函數裡面 執行 另一個 Generator 函數

 function* foo() {
    yield 'aaa'
    yield 'bbb'
  }
  function* bar() {
    yield* foo()      // 在bar函數中 **執行** foo函數
    yield 'ccc'
    yield 'ddd'
  }
  let iterator = bar()
  for(let value of iterator) {
    console.log(value)
  }
  // aaa
  // bbb
  // ccc
  // ddd
登入後複製

next() 方法的引數

yield表示式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個引數,該引數就會被當作上一個yield表示式的返回值

  function* gen(x) {
    let y = 2 * (yield (x + 1))   // 注意:yield 表示式如果用在另一個表示式中,必須放在圓括號裡面
    let z = yield (y / 3)
    return x + y + z
  }
  let it = gen(5)
  /*** 正確的結果在這裡 ***/
  console.log(it.next())  // 首次呼叫next,函數只會執行到 「yield(5+1)」 暫停,並返回 {value: 6, done: false}
  console.log(it.next())  // 第二次呼叫next,沒有傳遞引數,所以 y的值是undefined,那麼 y/3 當然是一個NaN,所以應該返回 {value: NaN, done: false}
  console.log(it.next())  // 同樣的道理,z也是undefined,6 + undefined + undefined = NaN,返回 {value: NaN, done: true}
登入後複製

如果向next方法提供引數,返回結果就完全不一樣了

{
  function* gen(x) {
    let y = 2 * (yield (x + 1))   // 注意:yield 表示式如果用在另一個表示式中,必須放在圓括號裡面
    let z = yield (y / 3)
    return x + y + z
  }
  let it = gen(5)
  console.log(it.next())  // 正常的運算應該是先執行圓括號內的計算,再去乘以2,由於圓括號內被 yield 返回 5 + 1 的結果並暫停,所以返回{value: 6, done: false}
  console.log(it.next(9))  // 上次是在圓括號內部暫停的,所以第二次呼叫 next方法應該從圓括號裡面開始,就變成了 let y = 2 * (9),y被賦值為18,所以第二次返回的應該是 18/3的結果 {value: 6, done: false}
  console.log(it.next(2))  // 引數2被賦值給了 z,最終 x + y + z = 5 + 18 + 2 = 25,返回 {value: 25, done: true}
}
登入後複製

與 Iterator 介面的關係

ES6 規定,預設的 Iterator 介面部署在資料結構的Symbol.iterator屬性,或者說,一個資料結構只要具有Symbol.iterator屬性,就可以認為是「可遍歷的」(iterable)。

Symbol.iterator屬性本身是一個函數,就是當前資料結構預設的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。

由於執行 Generator 函數實際返回的是一個遍歷器,因此可以把 Generator 賦值給物件的Symbol.iterator屬性,從而使得該物件具有 Iterator 介面。

{
  let obj = {}
  function* gen() {
    yield 4
    yield 5
    yield 6
  }
  obj[Symbol.iterator] = gen
  for(let value of obj) {
    console.log(value)
  }
  // 4
  // 5
  // 6
  console.log([...obj])    // [4, 5, 6]
}
登入後複製

傳統物件沒有原生部署 Iterator介面,不能使用 for...of 和 擴充套件運運算元,現在通過給物件新增Symbol.iterator 屬性和對應的遍歷器生成函數,就可以使用了

for...of 迴圈

由於 Generator 函數執行時生成的是一個 Iterator 物件,因此,可以直接使用 for...of 迴圈遍歷,且此時無需再呼叫 next() 方法

這裡需要注意,一旦 next() 方法的返回物件的 done 屬性為 true,for...of 迴圈就會終止,且不包含該返回物件

{
  function* gen() {
    yield 1
    yield 2
    yield 3
    yield 4
    return 5
  }
  for(let item of gen()) {
    console.log(item)
  }
  // 1 2 3 4
}
登入後複製

Generator.prototype.return()

Generator 函數返回的遍歷器物件,還有一個 return 方法,可以返回給定的值(若沒有提供引數,則返回值的value屬性為 undefined),並且 終結 遍歷 Generator 函數


{
  function* gen() {
    yield 1
    yield 2
    yield 3
  }
  let it = gen()
  it.next()             // {value: 1, done: false}
  it.return('ending')   // {value: "ending", done: true}
  it.next()             // {value: undefined, done: true}
}
登入後複製


Generator 函數應用舉例

應用一:假定某公司的年會上有一個抽獎活動,總共6個人可以抽6次,每抽一次,抽獎機會就會遞減

按照常規做法就需要宣告一個全域性的變數來儲存剩餘的可抽獎次數,而全域性變數會造成全域性汙染,指不定什麼時候就被重新賦值了,所以往往並不被大家推薦


{
  let count = 6  // 宣告一個全域性變數
  // 具體抽獎邏輯的方法
  function draw() {
    // 執行一段抽獎邏輯
    // ...
    // 執行完畢
    console.log(`剩餘${count}次`)
  }
  // 執行抽獎的方法
  function startDrawing(){
    if(count > 0) {
      count--
      draw(count)
    }
  }
  let btn = document.createElement('button')
  btn.id = 'start'
  btn.textContent = '開始抽獎'
  document.body.appendChild(btn)
  document.getElementById('start').addEventListener('click', function(){
    startDrawing()
  }, false)
}[object Object]
登入後複製

事實上,抽獎通常是每個人自己來抽,每抽一次就呼叫一次抽獎方法,而不是點一次就一次性就全部執行完,是可暫停的,這個不就是 Generator 函數的意義所在嗎?

  // 具體抽獎邏輯的方法
  function draw(count) {
    // 執行一段抽獎邏輯
    // ...
    console.log(`剩餘${count}次`)
  }
  // 執行抽獎的方法
  function* remain(count) {
    while(count > 0) {
      count--
      yield draw(count)
    }
  }
  let startDrawing = remain(6)
  let btn = document.createElement('button')
  btn.id = 'start'
  btn.textContent = '開始抽獎'
  document.body.appendChild(btn)
  document.getElementById('start').addEventListener('click', function(){
    startDrawing.next()
  }, false)
登入後複製

應用二:由於HTTP是一種無狀態協定,執行一次請求後伺服器無法記住是從哪個使用者端發起的請求,因此當需要實時把伺服器資料更新到使用者端時通常採用的方法是長輪詢和Websocket。這裡也可以用 Generator 函數來實現長輪詢

{
  // 請求的方法
  function* ajax() {
    yield new Promise((resolve, reject) => {
      // 此處用一個定時器來模擬請求資料的耗時,並約定當返回的json中code為0表示有新資料更新
      setTimeout(() => {
        resolve({code: 0})
      }, 200)
    })
  }
  // 長輪詢的方法
  function update() {
    let promise = ajax().next().value    // 返回的物件的value屬性是一個 Promise 範例物件
    promise.then(res => {
      if(res.code != 0) {
        setTimeout(() => {
          console.log('2秒後繼續查詢.....')
          update()
        }, 2000)
      } else{
        console.log(res)
      }
    })
  }
  update()
}
登入後複製

【相關推薦:、】

以上就是JavaScript中Generator函數yield表示式範例詳解的詳細內容,更多請關注TW511.COM其它相關文章!