轉載請註明出處: "Generator函數語法解析" Generator函數是ES6提供的一種非同步編程解決方案,語法與傳統函數完全不同。以下會介紹一下Generator函數。 寫下這篇文章的目的其實很簡單,是想梳理一下自己對於Generator的理解,同時呢,為學習async函數做一下知識儲備。 G ...
轉載請註明出處: Generator函數語法解析
Generator函數是ES6提供的一種非同步編程解決方案,語法與傳統函數完全不同。以下會介紹一下Generator函數。
寫下這篇文章的目的其實很簡單,是想梳理一下自己對於Generator的理解,同時呢,為學習async函數做一下知識儲備。
Generator函數
- 基本概念
- yield表達式
- next方法
- next方法的參數
- yield*表達式
- 與Iterator介面的關係
- for...of迴圈
- 作為對象屬性的Generator函數
- Generator函數中的this
- 應用
基本概念
對於Generator函數(也可以叫做生成器函數)的理解,可以從四個方面:
形式上:Generator函數是一個普通的函數,不過相對於普通函數多出了兩個特征。一是在function關鍵字和函數明之間多了'*'號;二是函數內部使用了yield表達式,用於定義Generator函數中的每個狀態。
語法上:Generator函數封裝了多個內部狀態(通過yield表達式定義內部狀態)。執行Generator函數時會返回一個遍歷器對象(Iterator對象)。也就是說,Generator是遍歷器對象生成函數,函數內部封裝了多個狀態。通過返回的Iterator對象,可以依次遍歷(調用next方法)Generator函數的每個內部狀態。
調用上:普通函數在調用之後會立即執行,而Generator函數調用之後不會立即執行,而是會返回遍歷器對象(Iterator對象)。通過Iterator對象的next方法來遍歷內部yield表達式定義的每一個狀態。
寫法上:*號放在哪裡好像都可以也。看個人習慣吧,我喜歡第一種寫法
function *gen () {} √
function* gen () {}
function * gen () {}
function*gen () {}
yield表達式
yield,英文意思即產生、退讓的意思,因此yield表達式也有兩種作用:定義內部狀態和暫停執行。
舉一個慄子吧: )
function *gen () {
yield 1
yield 2
return 3
}
const g = gen() // Iterator對象
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
從上面代碼中可以看出,gen函數使用yield表達式定義了兩個內部狀態。同時呢,也可以看出來,return語句只能有一個,而yield表達式卻可以有多個。
執行gen函數之後,會返回一個遍歷器對象,而不是立即執行gen函數。如果需要獲取yield表達式定義的每個狀態,需要調用next方法。
每調用一次next方法都會返回一個包含value和done屬性的對象,此時會停留在某個yield表達式結尾處。value屬性值即是yield表達式的值;done屬性是布爾值,表示是否遍歷完畢。
另外呢,yield表達式沒有返回值,或者說返回值是undefined。待會會說明一下如何給yield表達式傳遞返回值。
需要註意的是,yield表達式的值,只有調用next方法時才能獲取到。因此等於為JavaScript提供了手動的'惰性求值'(Lazy Evaluation)的功能。
一般情況下,Generator函數會結合yield表達式使用,通過yield表達式定義多個內部狀態。但是,如果不使用yield表達式的Generator函數就成為了一個單純的暫緩執行函數,個人感覺沒什麼意義...
function *gen () {
console.log('凱斯')
}
window.setTimeout(() => {
gen().next()
}, 2000)
// 不使用yield表達式來暫停函數的執行,還不如使用普通函數呢..
// 所以Generator函數配合yield表達式使用效果更佳
另外,yield表達式如果用在另一個表達式中,需要為其加上圓括弧。作為函數參數和語句是可以不使用圓括弧。
function *gen () {
console.log('hello' + yield) ×
console.log('hello' + (yield)) √
console.log('hello' + yield '凱斯') ×
console.log('hello' + (yield '凱斯')) √
foo(yield 1) √
const param = yield 2 √
}
next方法
yield表達式具有暫停執行的功能,而恢復執行的是next方法。每一次調用next方法,就會從函數頭部或者上一次停下來的地方開始執行,直到遇到下一個yield表達式(return 語句)為止。同時,調用next方法時,會返回包含value和done屬性的對象,value屬性值可以為yield表達式、return語句後面的值或者undefined值,done屬性表示遍歷是否結束。
遍歷器對象的next方法(從Generator函數繼承而來)的運行邏輯如下
- 遇到yield表達式,就暫停執行後面的操作,並將緊跟在yield表達式後面的那個表達式的值,作為返回的對象的value屬性值。
- 下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。
- 如果沒有再遇到新的yield表達式,就一直運行到函數結束,直到遇到return語句為止,並將return語句後面表達式的值,作為返回的對象的value屬性值。
- 如果該函數沒有return語句,則返回的對象的value屬性值為undefined。
從上面的運行邏輯可以看出,返回的對象的value屬性值有三種結果:
- yield表達式後面的值
- return語句後面的值
- undefined
也就是說,如果有yield表達式,則value屬性值就是yield表達式後面的指;如果沒有yield表達式,value屬性值就等於return語句後面的值;如果yield表達式和return語句都不存在的話,則value屬性值就等於undefined。舉個例子: )
function *gen () {
yield 1
yield 2
return 3
}
const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
g.next() // {value: undefined, done: true}
根據next運行邏輯再針對這個例子,就很容易理解了。調用gen函數,返回遍歷器對象。
第一次調用next方法時,在遇到第一個yield表達式時停止執行,value屬性值為1,即yield表達式後面的值,done為false表示遍歷沒有結束;
第二次調用next方法時,從暫停的yield表達式後開始執行,直到遇到下一個yield表達式後暫停執行,value屬性值為2,done為false;
第三次調用next方法時,從上一次暫停的yield表達式後開始執行,由於後面沒有yield表達式了,所以遇到return語句時函數執行結束,value屬性值為return語句後面的值,done屬性值為true表示已經遍歷完畢了。
第四次調用next方法時,value屬性值就是undefined了,此時done屬性為true表示遍歷完畢。以後再調用next方法都會是這兩個值。
next方法的參數
yield表達式本身沒有返回值,或者說總是返回undefined。
function *gen () {
var x = yield 'hello world'
var y = x / 2
return [x, y]
}
const g = gen()
g.next() // {value: 'hello world', done: false}
g.next() // {value: [undefined, NaN], done: true}
從上面代碼可以看出,第一次調用next方法時,value屬性值是'hello world',第二次調用時,由於變數y的值依賴於變數x,而yield表達式沒有返回值,所以返回了undefined給變數x,此時undefined / 2為NaN。
要解決上面的問題,可以給next方法傳遞參數。next方法可以帶一個參數,該參數就會被當作上一個yield表達式的返回值。
function *gen () {
var x = yield 'hello world'
var y = x / 2
return [x, y]
}
const g = gen()
g.next() // {value: 'hello world', done: false}
g.next(10) // {value: [10, 5], done: true}
當給第二個next方法傳遞參數10時,yield表達式的返回值為10,即var x = 10
,所以此時變數y為5。
註意,由於next方法的參數表示上一個yield表達式的返回值,所以在第一次使用next方法時,傳遞參數是無效的。V8引擎直接忽略第一次使用next方法的參數,只有從第二次使用next方法開始,參數才是有效的。從語義上說,第一個next方法用來啟動遍歷器對象,所以不用帶上參數。所以呢,每次使用next方法會比yield表達式要多一次。
如果想要第一次調用next方法時就可以傳遞參數,可以使用閉包的方式。
// 實際上就是在閉包內部執行了一次next方法
function wrapper (gen) {
return function (...args) {
const genObj = gen(...args)
genObj.next()
return genObj
}
}
const generator = wrapper(function *generator () {
console.log(`hello ${yield}`)
return 'done'
})
const a = generator().next('keith')
console.log(a) // hello keith, done
yield*表達式
如果在Generator函數中調用另一個Generator函數,預設情況下是無效的。
function *foo () {
yield 1
}
function *gen () {
foo()
yield 2
}
const g = gen()
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}
從上面代碼中可以看出,並沒有在yield 1
處停止執行。此時就需要使用yield* 表達式。從語法角度上說,如果yield表達式後面跟著遍歷器對象,需要在yield表達式後面加上星號,表明它返回的是一個遍歷器對象。實際上,yield*表達式是for...of迴圈的簡寫,完全可以使用for...of迴圈來代替yield*表達式
function *foo () {
yield 1
}
function *gen () {
yield* foo()
yield 2
}
const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}
// 相當於
function *gen () {
yield 1
yield 2
}
// 相當於
function *gen () {
for (let item of foo()) {
yield item
}
yield 2
}
如果直接使用了yield foo()
,返回的對象的value屬性值為一個遍歷器對象。而不是Generator函數的內部狀態。
function *foo () {
yield 1
}
function *gen () {
yield foo()
yield 2
}
const g = gen()
g.next() // {value: Generator, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}
另外,任何數據類型(Array, String)只要有Iterator介面,就能夠被yield*
遍歷
const arr = ['a', 'b']
const str = 'keith'
function *gen () {
yield arr
yield* arr
yield str
yield* str
}
const g = gen()
g.next() // {value: ['a', 'b'], done: false}
g.next() // {value: 'a', done: false}
g.next() // {value: 'b', done: false}
g.next() // {value: 'keith', done: false}
g.next() // {value: 'k', done: false}
...
如果在Generator函數中存在return語句,則需要使用let value = yield* iterator
方式獲取返回值。
function *foo () {
yield 1
return 2
}
function *gen () {
var x = yield* foo()
return x
}
const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: true}
使用yield*表達式可以很方便的取出嵌套數組的成員。
// 普通方法
const arr = [1, [[2, 3], 4]]
const str = arr.toString().replace(/,/g, '')
for (let item of str) {
console.log(+item) // 1, 2, 3, 4
}
// 使用yield*表達式
function *gen (arr) {
if (Array.isArray(arr)) {
for (let i = 0; i < arr.length; i++) {
yield * gen(arr[i])
}
} else {
yield arr
}
}
const g = gen([1, [[2, 3], 4]])
for (let item of g) {
console.log(item) // 1, 2, 3, 4
}
與Iterator介面的關係
任何一個對象的Symbol.iterator
屬性,指向預設的遍歷器對象生成函數。而Generator函數也是遍歷器對象生成函數,所以可以將Generator函數賦值給Symbol.iterator
屬性,這樣就使對象具有了Iterator介面。預設情況下,對象是沒有Iterator介面的。
具有Iterator介面的對象,就可以被擴展運算符(...),解構賦值,Array.from和for...of迴圈遍歷了。
const person = {
name: 'keith',
height: 180
}
function *gen () {
const arr = Object.keys(this)
for (let item of arr) {
yield [item, this[item]]
}
}
person[Symbol.iterator] = gen
for (let [key, value] of person) {
console.log(key, value) // name keith , height 180
}
Generator函數函數執行之後,會返回遍歷器對象。該對象本身也就有Symbol.iterator
屬性,執行後返回自身
function *gen () {}
const g = gen()
g[Symbol.iterator]() === g // true
for...of迴圈
for...of迴圈可以自動遍歷Generator函數生成的Iterator對象,不用調用next方法。
function *gen () {
yield 1
yield 2
yield 3
return 4
}
for (let item of gen()) {
console.log(item) // 1 2 3
}
上面代碼使用for...of迴圈,依次顯示 3 個yield表達式的值。這裡需要註意,一旦next方法的返回對象的done屬性為true,for...of迴圈就會中止,且不包含該返回對象,所以上面代碼的return語句返回的6,不包括在for...of迴圈之中。
作為對象屬性的Generator函數
如果一個對象有Generator函數,那麼可以使用簡寫方式
let obj = {
* gen () {}
}
// 也可以完整的寫法
let obj = {
gen: function *gen () {}
}
當然了,如果是在構造函數中,簡寫形式也是一樣的。
class F {
* gen () {}
}
Generator函數中的this
Generator函數中的this對象跟構造函數中的this對象有異曲同工之處。先來看看構造函數中的new關鍵字的工作原理。
function F () {
this.a = 1
}
const f = new F()
- 調用構造函數F,返回實例對象f
- 將構造函數內部中的this指向這個實例對象
- 將構造函數中的原型對象賦值給實例對象的原型
- 執行構造函數中的代碼
調用Generator函數會返回遍歷器對象,而不是實例對象,因此無法獲取到this指向的實例對象上的私有屬性和方法。但是這個遍歷器對象可以繼承Generator函數的prototype原型對象上的屬性和方法(公有屬性和方法)。
function *Gen () {
yield this.a = 1
}
Gen.prototype.say = function () {
console.log('keith')
}
const g = new Gen()
g.a // undefined
g.say() // 'keith'
如果希望修複this指向性問題,可以使用call方法將函數執行時所在的作用域綁定到Generator.prototype原型對象上。這樣做,會使私有屬性和方法變成公有的了,因為都在原型對象上了。
function *Gen () {
this.a = 1
yield this.b = 2
yield this.c = 3
}
const g = Gen.call(Gen.prototype)
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: false}
g.next() // {value: undefined, done: true}
g.a // 1,繼承自Gen.prototype
g.b // 2,同上
g.c // 3,同上
應用
Generator函數的應用主要在非同步編程上,會在下一篇文章中分享。請期待噢: )