1、什么是 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方法時,內部指針從函數頭部開始執行,遇到第一個 yield 表達式暫停,並返回當前狀態的值 'hello'
第二次調用next方法時,內部指針從上一個(即第一個) yield 表達式開始,遇到第二個 yield 表達式暫停,返回當前狀態的值 'world'
第三次調用next方法時,內部指針從第二個 yield 表達式開始,遇到return語句暫停,返回當前狀態的值 'end',同時所有狀態遍歷完畢,done 屬性的值變為true
第四次調用next方法時,由於函數已經遍歷運行完畢,不再有其它狀態,因此返回 {value: undefined, done: true}。如果繼續調用next方法,返回的也都是這個值
2、yield 表達式
(1)、yield 表達式只能用在 Generator 函數里面,用在其它地方都會報錯
{ (function (){ yield 1; })() // SyntaxError: Unexpected number // 在一個普通函數中使用yield表達式,結果產生一個句法錯誤 }
(2)、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 } }
(3)、yield 表達式用作參數或放在賦值表達式的右邊,可以不加括號
{ function* demo() { foo(yield 'a', yield 'b'); // OK let input = yield; // OK } }
(4)、yield 表達式和return語句的區別
相似:都能返回緊跟在語句后面的那個表達式的值
區別:
-- 每次遇到 yield,函數就暫停執行,下一次再從該位置繼續向后執行;而 return 語句不具備記憶位置的功能
-- 一個函數只能執行一次 return 語句,而在 Generator 函數中可以有任意多個 yield
3、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 }
4、next() 方法的參數
yield表達式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個參數,該參數就會被當作上一個yield表達式的返回值
[rv] = yield [expression]
expression:定義通過遍歷器從生成器函數返回的值,如果省略,則返回 undefined
rv:接收從下一個 next() 方法傳遞來的參數
先看一個簡單的小栗子,並嘗試解析遍歷生成器函數的執行過程
{ function* gen() { let result = yield 3 + 5 + 6 console.log(result) yield result } let it = gen() console.log(it.next()) // {value: 14, done: false} console.log(it.next()) // undefined {value: undefined, done: false} }
(此處分析過程純屬個人理解,有誤之處,歡迎批評指正!)
第一次調用遍歷器對象的next方法,函數從頭部開始執行,遇到第一個 yield 暫停,在這個過程中其實是分了三步:
(1)、聲明了一個變量result,並將聲明提前,默認值為 undefined
(2)、由於 Generator函數是 “
惰性求值”,執行到第一個 yield 時才會計算求和,並加計算結果返回給遍歷器對象 {value: 14, done: false},函數暫停運行
(3)、理論上應該要把等號右邊的 [yield 3 + 5 + 6] 賦值給變量result,但是,由於函數執行到 yield 時暫定了,這一步就被掛起了
第二次調用next方法,函數從上一次 yield 停下的地方開始執行,也就是給result賦值的地方開始,由於next()並沒有傳參,就相當於傳參為undefined

基於以上分析,就不難理解為什么說 yield
表達式本身的返回值(特指 [rv])總是undefined了。現在把上面的代碼稍作修改,第二次調用 next() 方法傳一個參數3,按照上圖分析可以很快得出輸出結果
{ function* gen() { let result = yield 3 + 5 + 6 console.log(result) yield result } let it = gen() console.log(it.next()) // {value: 14, done: false} console.log(it.next(3)) // 3 {value: 3, done: false} }
如果第一次調用next()的時候也傳了一個參數呢?這個當然是無效的,next方法的參數表示上一個yield表達式的返回值,所以在第一次使用next方法時,傳遞參數是無效的。
從語義上講,第一個next方法用來啟動遍歷器對象,所以不用帶有參數。
{ function* gen() { let result = yield 3 + 5 + 6 console.log(result) yield result } let it = gen() console.log(it.next(10)) // {value: 14, done: false} console.log(it.next(3)) // 3 {value: 3, done: false} }
Generator 函數從暫停狀態到恢復運行,它的上下文狀態(context)是不變的。通過next方法的參數,就有辦法在 Generator 函數開始運行之后,繼續向函數體內部注入值。也就是說,可以在 Generator 函數運行的不同階段,從外部向內部注入不同的值,從而調整函數行為。
{ 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()) // {value: 6, done: false} console.log(it.next()) // {value: 2, done: false} console.log(it.next()) // {value: 13, done: false} */ /*** 正確的結果在這里 ***/ 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} }
{ function* gen(x) { let y = 2 * (yield (x + 1)) let z = yield (y / 3) z = 88 // 注意看這里 return x + y + z } let it = gen(5) console.log(it.next()) // {value: 6, done: false} console.log(it.next(9)) // {value: 6, done: false} console.log(it.next(2)) // 這里其實也很容易理解,參數2被賦值給了 z,但是函數體內又給 z 重新賦值為88, 最終 x + y + z = 5 + 18 + 88 = 111,返回 {value: 111, done: true} }
5、與 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 屬性和對應的遍歷器生成函數,就可以使用了
6、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 }
7、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) }
事實上,抽獎通常是每個人自己來抽,每抽一次就調用一次抽獎方法,而不是點一次就一次性就全部運行完,是可暫停的,這個不就是 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() }