Generator函數語法解析

轉載請註明出處:html

Generator函數語法解析-掘金專欄es6

Generator函數語法解析-知乎專欄編程

Generator函數語法解析-博客園數組

Generator函數是ES6提供的一種異步編程解決方案,語法與傳統函數徹底不一樣。如下會介紹一下Generator函數。bash

寫下這篇文章的目的其實很簡單,是想梳理一下本身對於Generator的理解,同時呢,爲學習async函數作一下知識儲備。閉包


Generator函數

  1. 基本概念
  2. yield表達式
  3. next方法
  4. next方法的參數
  5. yield*表達式
  6. 與Iterator接口的關係
  7. for...of循環
  8. 做爲對象屬性的Generator函數
  9. Generator函數中的this
  10. 應用

基本概念

對於Generator函數(也能夠叫作生成器函數)的理解,能夠從四個方面:app

形式上: Generator函數是一個普通的函數,不過相對於普通函數多出了兩個特徵。一是在function關鍵字和函數明之間多了'*'號;二是函數內部使用了yield表達式,用於定義Generator函數中的每一個狀態。異步

語法上: Generator函數封裝了多個內部狀態(經過yield表達式定義內部狀態)。執行Generator函數時會返回一個遍歷器對象(Iterator對象)。也就是說,Generator是遍歷器對象生成函數,函數內部封裝了多個狀態。經過返回的Iterator對象,能夠依次遍歷(調用next方法)Generator函數的每一個內部狀態。async

調用上: 普通函數在調用以後會當即執行,而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函數繼承而來)的運行邏輯以下

  1. 遇到yield表達式,就暫停執行後面的操做,並將緊跟在yield表達式後面的那個表達式的值,做爲返回的對象的value屬性值。
  2. 下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。
  3. 若是沒有再遇到新的yield表達式,就一直運行到函數結束,直到遇到return語句爲止,並將return語句後面表達式的值,做爲返回的對象的value屬性值。
  4. 若是該函數沒有return語句,則返回的對象的value屬性值爲undefined。

從上面的運行邏輯能夠看出,返回的對象的value屬性值有三種結果:

  1. yield表達式後面的值
  2. return語句後面的值
  3. 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表達式和next方法構成了雙向信息傳遞。**yield表達式能夠向外傳遞value值,而next方法參數能夠向內傳遞值。

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.fromfor...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()
複製代碼
  1. 調用構造函數F,返回實例對象f
  2. 將構造函數內部中的this指向這個實例對象
  3. 將構造函數中的原型對象賦值給實例對象的原型
  4. 執行構造函數中的代碼

調用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函數的應用主要在異步編程上,會在下一篇文章中分享。請期待噢: )

相關文章
相關標籤/搜索