generator

概念

Generator 函數有多種理解角度。語法上,首先能夠把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。es6

執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,仍是一個遍歷器對象生成函數。返回的遍歷器對象,能夠依次遍歷 Generator 函數內部的每個狀態。bash

基本使用

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
複製代碼

上面代碼定義了一個 Generator 函數helloWorldGenerator,它內部有兩個yield表達式(hello和world),即該函數有三個狀態:hello,world 和 return 語句(結束執行)。app

而後,Generator 函數的調用方法與普通函數同樣,也是在函數名後面加上一對圓括號。不一樣的是,調用 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象,也就是遍歷器對象(Iterator Object)。函數

下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式(或return語句)爲止。換言之,Generator 函數是分段執行的,yield表達式是暫停執行的標記,而next方法能夠恢復執行。ui

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }
複製代碼

上面代碼一共調用了四次next方法。this

第一次調用,Generator 函數開始執行,直到遇到第一個yield表達式爲止。next方法返回一個對象,它的value屬性就是當前yield表達式的值hello,done屬性的值false,表示遍歷尚未結束。lua

第二次調用,Generator 函數從上次yield表達式停下的地方,一直執行到下一個yield表達式。next方法返回的對象的value屬性就是當前yield表達式的值world,done屬性的值false,表示遍歷尚未結束。spa

第三次調用,Generator 函數從上次yield表達式停下的地方,一直執行到return語句(若是沒有return語句,就執行到函數結束)。next方法返回的對象的value屬性,就是緊跟在return語句後面的表達式的值(若是沒有return語句,則value屬性的值爲undefined),done屬性的值true,表示遍歷已經結束。prototype

第四次調用,此時 Generator 函數已經運行完畢,next方法返回對象的value屬性爲undefined,done屬性爲true。之後再調用next方法,返回的都是這個值。指針

總結一下,調用 Generator 函數,返回一個遍歷器對象,表明 Generator 函數的內部指針。之後,每次調用遍歷器對象的next方法,就會返回一個有着value和done兩個屬性的對象。value屬性表示當前的內部狀態的值,是yield表達式後面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。

yield 表達式

因爲 Generator 函數返回的遍歷器對象,只有調用next方法纔會遍歷下一個內部狀態,因此其實提供了一種能夠暫停執行的函數。yield表達式就是暫停標誌。

遍歷器對象的next方法的運行邏輯以下。

(1)遇到yield表達式,就暫停執行後面的操做,並將緊跟在yield後面的那個表達式的值,做爲返回的對象的value屬性值。

(2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。

(3)若是沒有再遇到新的yield表達式,就一直運行到函數結束,直到return語句爲止,並將return語句後面的表達式的值,做爲返回的對象的value屬性值。

(4)若是該函數沒有return語句,則返回的對象的value屬性值爲undefined。

須要注意的是,yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行,所以等於爲 JavaScript 提供了手動的「惰性求值」(Lazy Evaluation)的語法功能。

function* gen() {
  yield  123 + 456;
}

上面代碼中,yield後面的表達式123 + 456,不會當即求值,只會在next方法將指針移到這一句時,纔會求值。


複製代碼

yield表達式與return語句既有類似之處,也有區別。類似之處在於,都能返回緊跟在語句後面的那個表達式的值。區別在於每次遇到yield,函數暫停執行,下一次再從該位置繼續向後執行,而return語句不具有位置記憶的功能。一個函數裏面,只能執行一次(或者說一個)return語句,可是能夠執行屢次(或者說多個)yield表達式。正常函數只能返回一個值,由於只能執行一次return;Generator 函數能夠返回一系列的值,由於能夠有任意多個yield。從另外一個角度看,也能夠說 Generator 生成了一系列的值,這也就是它的名稱的來歷(英語中,generator 這個詞是「生成器」的意思)。

Generator 函數能夠不用yield表達式,這時就變成了一個單純的暫緩執行函數

function* f() {
  console.log('執行了!')
}

var generator = f();

setTimeout(function () {
  generator.next()
}, 2000);
複製代碼

上面代碼中,函數f若是是普通函數,在爲變量generator賦值時就會執行。可是,函數f是一個 Generator 函數,就變成只有調用next方法時,函數f纔會執行。

另外須要注意,yield表達式只能用在 Generator 函數裏面,用在其餘地方都會報錯。

next 方法的參數

yield表達式自己沒有返回值,或者說老是返回undefined。next方法能夠帶一個參數,該參數就會被看成上一個yield表達式的返回值。

上面代碼先定義了一個能夠無限運行的 Generator 函數f,若是next方法沒有參數,每次運行到yield表達式,變量reset的值老是undefined。當next方法帶一個參數true時,變量reset就被重置爲這個參數(即true),所以i會等於-1,下一輪循環就會從-1開始遞增。

這個功能有很重要的語法意義。Generator 函數從暫停狀態到恢復運行,它的上下文狀態(context)是不變的。經過next方法的參數,就有辦法在 Generator 函數開始運行以後,繼續向函數體內部注入值。也就是說,能夠在 Generator 函數運行的不一樣階段,從外部向內部注入不一樣的值,從而調整函數行爲。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
複製代碼

上面代碼中,第二次運行next方法的時候不帶參數,致使 y 的值等於2 * undefined(即NaN),除以 3 之後仍是NaN,所以返回對象的value屬性也等於NaN。第三次運行Next方法的時候不帶參數,因此z等於undefined,返回對象的value屬性等於5 + NaN + undefined,即NaN。

若是向next方法提供參數,返回結果就徹底不同了。上面代碼第一次調用b的next方法時,返回x+1的值6;第二次調用next方法,將上一次yield表達式的值設爲12,所以y等於24,返回y / 3的值8;第三次調用next方法,將上一次yield表達式的值設爲13,所以z等於13,這時x等於5,y等於24,因此return語句的值等於42。

注意,因爲next方法的參數表示上一個yield表達式的返回值,因此在第一次使用next方法時,傳遞參數是無效的。V8 引擎直接忽略第一次使用next方法時的參數,只有從第二次使用next方法開始,參數纔是有效的。從語義上講,第一個next方法用來啓動遍歷器對象,因此不用帶有參數。

function* dataConsumer() {
  console.log('Started');
  console.log(`1. ${yield}`);
  console.log(`2. ${yield}`);
  return 'result';
}

let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b

複製代碼

若是想要第一次調用next方法時,就可以輸入值,能夠在 Generator 函數外面再包一層。

function wrapper(generatorFunction) {
  return function (...args) {
    let generatorObject = generatorFunction(...args);
    generatorObject.next();
    return generatorObject;
  };
}

const wrapped = wrapper(function* () {
  console.log(`First input: ${yield}`);
  return 'DONE';
});

wrapped().next('hello!')
// First input: hello!
複製代碼

for...of 循環

for...of循環能夠自動遍歷 Generator 函數運行時生成的Iterator對象,且此時再也不須要調用next方法。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5
複製代碼

上面代碼使用for...of循環,依次顯示 5 個yield表達式的值。這裏須要注意,一旦next方法的返回對象的done屬性爲true,for...of循環就會停止,且不包含該返回對象,因此上面代碼的return語句返回的6,不包括在for...of循環之中。

下面是一個利用 Generator 函數和for...of循環,實現斐波那契數列的例子。

function* fibonacci() {
  let [prev, curr] = [0, 1];
  for (;;) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

for (let n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}
複製代碼

利用for...of循環,能夠寫出遍歷任意對象(object)的方法。原生的 JavaScript 對象沒有遍歷接口,沒法使用for...of循環,經過 Generator 函數爲它加上這個接口,就能夠用了。

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
複製代碼

上面代碼中,對象jane原生不具有 Iterator 接口,沒法用for...of遍歷。這時,咱們經過 Generator 函數objectEntries爲它加上遍歷器接口,就能夠用for...of遍歷了。加上遍歷器接口的另外一種寫法是,將 Generator 函數加到對象的Symbol.iterator屬性上面。

function* objectEntries() {
  let propKeys = Object.keys(this);

  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
複製代碼

除了for...of循環之外,擴展運算符(...)、解構賦值和Array.from方法內部調用的,都是遍歷器接口。這意味着,它們均可以將 Generator 函數返回的 Iterator 對象,做爲參數。

function* numbers () {
  yield 1
  yield 2
  return 3
  yield 4
}

// 擴展運算符
[...numbers()] // [1, 2]

// Array.from 方法
Array.from(numbers()) // [1, 2]

// 解構賦值
let [x, y] = numbers();
x // 1
y // 2

// for...of 循環
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2
複製代碼

Generator.prototype.throw()

Generator 函數返回的遍歷器對象,都有一個throw方法,能夠在函數體外拋出錯誤,而後在 Generator 函數體內捕獲。

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('內部捕獲', e);
  }
};

var i = g();
i.next();

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b
複製代碼

上面代碼中,遍歷器對象i連續拋出兩個錯誤。第一個錯誤被 Generator 函數體內的catch語句捕獲。i第二次拋出錯誤,因爲 Generator 函數內部的catch語句已經執行過了,不會再捕捉到這個錯誤了,因此這個錯誤就被拋出了 Generator 函數體,被函數體外的catch語句捕獲。

throw方法能夠接受一個參數,該參數會被catch語句接收,建議拋出Error對象的實例。

更多請查看 阮一峯老師的 generator用法

相關文章
相關標籤/搜索