ECMAScript 6 入門 ----Generator 函數

本文轉自:阮一峯老師的ECMAScript 6 入門,有時間能夠看下評論!javascript

Generator 函數

簡介

基本概念

Generator函數是ES6提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣。本章詳細介紹Generator函數的語法和API,它的異步編程應用請看《異步操做》一章。java

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

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

形式上,Generator函數是一個普通函數,可是有兩個特徵。一是,function命令與函數名之間有一個星號;二是,函數體內部使用yield語句,定義不一樣的內部狀態(yield語句在英語裏的意思就是「產出」)。算法

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

var hw = helloWorldGenerator();

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

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

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

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方法。安全

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

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

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

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

總結一下,調用Generator函數,返回一個遍歷器對象,表明Generator函數的內部指針。之後,每次調用遍歷器對象的next方法,就會返回一個有着valuedone兩個屬性的對象。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語句不能用在普通函數中,不然會報錯。

(function (){
  yield 1;
})()
// SyntaxError: Unexpected number

上面代碼在一個普通函數中使用yield語句,結果產生一個句法錯誤。

下面是另一個例子。

var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a){
  a.forEach(function(item){
    if (typeof item !== 'number'){
      yield* flat(item);
    } else {
      yield item;
    }
  }
};

for (var f of flat(arr)){
  console.log(f);
}

上面代碼也會產生句法錯誤,由於forEach方法的參數是一個普通函數,可是在裏面使用了yield語句。一種修改方法是改用for循環。

var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a){
  var length = a.length;
  for(var i =0;i<length;i++){
    var item = a[i];
    if (typeof item !== 'number'){
      yield* flat(item);
    } else {
      yield item;
    }
  }
};

for (var f of flat(arr)){
  console.log(f);
}
// 1, 2, 3, 4, 5, 6

另外,yield語句若是用在一個表達式之中,必須放在圓括號裏面。

console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError

console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK

yield語句用做函數參數或賦值表達式的右邊,能夠不加括號。

foo(yield 'a', yield 'b'); // OK
let input = yield; // OK

與Iterator接口的關係

上一章說過,任意一個對象的Symbol.iterator方法,等於該對象的遍歷器對象生成函數,調用該函數會返回該對象的一個遍歷器對象。

遍歷器對象自己也有Symbol.iterator方法,執行後返回自身。

function* gen(){
  // some code
}

var g = gen();

g[Symbol.iterator]() === g
// true

上面代碼中,gen是一個Generator函數,調用它會生成一個遍歷器對象g。它的Symbol.iterator屬性,也是一個遍歷器對象生成函數,執行後返回它本身。

next方法的參數

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

function* f() {
  for(var i=0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }

上面代碼先定義了一個能夠無限運行的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:false}

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

若是向next方法提供參數,返回結果就徹底不同了。

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

var it = foo(5);

it.next()
// { value:6, done:false }
it.next(12)
// { value:8, done:false }
it.next(13)
// { value:42, done:true }

上面代碼第一次調用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方法時,就可以輸入值,能夠在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!

上面代碼中,Generator函數若是不用wrapper先包一層,是沒法第一次調用next方法,就輸入參數的。

for…of循環

for...of循環能夠自動遍歷Generator函數,且此時再也不須要調用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屬性爲truefor...of循環就會停止,且不包含該返回對象,因此上面代碼的return語句返回的6,不包括在for...of循環之中。

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

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

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

從上面代碼可見,使用for...of語句時不須要使用next方法。

前面章節曾經介紹過,for...of循環、擴展運算符(…)、解構賦值和Array.from方法內部調用的,都是遍歷器接口。這意味着,它們能夠將Generator函數返回的Iterator對象,做爲參數。

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

[...numbers()] // [1, 2]

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

let [x, y] = numbers();
x // 1
y // 2

for (let n of numbers()) {
  console.log(n)
}
// 1
// 2

利用for...of循環,能夠寫出遍歷任意對象的方法。原生的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

Generator.prototype.throw()

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

var g = function* () {
  while (true) {
    try {
      yield;
    } catch (e) {
      if (e != 'a') throw 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語句捕獲,而後Generator函數執行完成,因而第二個錯誤被函數體外的catch語句捕獲。

注意,不要混淆遍歷器對象的throw方法和全局的throw命令。上面代碼的錯誤,是用遍歷器對象的throw方法拋出的,而不是用throw命令拋出的。後者只能被函數體外的catch語句捕獲。

var g = function* () {
  while (true) {
    try {
      yield;
    } catch (e) {
      if (e != 'a') throw e;
      console.log('內部捕獲', e);
    }
  }
};

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

try {
  throw new Error('a');
  throw new Error('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 外部捕獲 [Error: a]

上面代碼之因此只捕獲了a,是由於函數體外的catch語句塊,捕獲了拋出的a錯誤之後,就不會再繼續執行try語句塊了。

若是Generator函數內部沒有部署try…catch代碼塊,那麼throw方法拋出的錯誤,將被外部try…catch代碼塊捕獲。

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

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

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

上面代碼中,遍歷器函數g內部,沒有部署try…catch代碼塊,因此拋出的錯誤直接被外部catch代碼塊捕獲。

若是Generator函數內部部署了try…catch代碼塊,那麼遍歷器的throw方法拋出的錯誤,不影響下一次遍歷,不然遍歷直接終止。

var gen = function* gen(){
  yield console.log('hello');
  yield console.log('world');
}

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

try {
  g.throw();
} catch (e) {
  g.next();
}
// hello

上面代碼只輸出hello就結束了,由於第二次調用next方法時,遍歷器狀態已經變成終止了。可是,若是使用throw命令拋出錯誤,不會影響遍歷器狀態。

var gen = function* gen(){
  yield console.log('hello');
  yield console.log('world');
}

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

try {
  throw new Error();
} catch (e) {
  g.next();
}
// hello
// world

上面代碼中,throw命令拋出的錯誤不會影響到遍歷器的狀態,因此兩次執行next方法,都取到了正確的操做。

這種函數體內捕獲錯誤的機制,大大方便了對錯誤的處理。若是使用回調函數的寫法,想要捕獲多個錯誤,就不得不爲每一個函數寫一個錯誤處理語句。

foo('a', function (a) {
  if (a.error) {
    throw new Error(a.error);
  }

  foo('b', function (b) {
    if (b.error) {
      throw new Error(b.error);
    }

    foo('c', function (c) {
      if (c.error) {
        throw new Error(c.error);
      }

      console.log(a, b, c);
    });
  });
});

使用Generator函數能夠大大簡化上面的代碼。

function* g(){
  try {
    var a = yield foo('a');
    var b = yield foo('b');
    var c = yield foo('c');
  } catch (e) {
    console.log(e);
  }

  console.log(a, b, c);
}

反過來,Generator函數內拋出的錯誤,也能夠被函數體外的catch捕獲。

function *foo() {
  var x = yield 3;
  var y = x.toUpperCase();
  yield y;
}

var it = foo();

it.next(); // { value:3, done:false }

try {
  it.next(42);
} catch (err) {
  console.log(err);
}

上面代碼中,第二個next方法向函數體內傳入一個參數42,數值是沒有toUpperCase方法的,因此會拋出一個TypeError錯誤,被函數體外的catch捕獲。

一旦Generator執行過程當中拋出錯誤,就不會再執行下去了。若是此後還調用next方法,將返回一個value屬性等於undefineddone屬性等於true的對象,即JavaScript引擎認爲這個Generator已經運行結束了。

function* g() {
  yield 1;
  console.log('throwing an exception');
  throw new Error('generator broke!');
  yield 2;
  yield 3;
}

function log(generator) {
  var v;
  console.log('starting generator');
  try {
    v = generator.next(); // { value: undefined, done: true } 
    console.log('第一次運行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  try {
    v = generator.next();
    console.log('第二次運行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  try {
    v = generator.next();
    console.log('第三次運行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  console.log('caller done');
}

log(g());
// starting generator
// 第一次運行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉錯誤 { value: 1, done: false }
// 第三次運行next方法 { value: undefined, done: true }
// caller done

上面代碼一共三次運行next方法,第二次運行的時候會拋出錯誤,而後第三次運行的時候,Generator函數就已經結束了,再也不執行下去了。

Generator.prototype.return()

Generator函數返回的遍歷器對象,還有一個return方法,能夠返回給定的值,而且終結遍歷Generator函數。

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return("foo") // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }

上面代碼中,遍歷器對象g調用return方法後,返回值的value屬性就是return方法的參數foo。而且,Generator函數的遍歷就終止了,返回值的done屬性爲true,之後再調用next方法,done屬性老是返回true

若是return方法調用時,不提供參數,則返回值的vaule屬性爲undefined

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return() // { value: undefined, done: true }

若是Generator函數內部有try...finally代碼塊,那麼return方法會推遲到finally代碼塊執行完再執行。

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers()
g.next() // { done: false, value: 1 }
g.next() // { done: false, value: 2 }
g.return(7) // { done: false, value: 4 }
g.next() // { done: false, value: 5 }
g.next() // { done: true, value: 7 }

上面代碼中,調用return方法後,就開始執行finally代碼塊,而後等到finally代碼塊執行完,再執行return方法。

yield*語句

若是在Generater函數內部,調用另外一個Generator函數,默認狀況下是沒有效果的。

function* foo() {
  yield 'a';
  yield 'b';
}

function* bar() {
  yield 'x';
  foo();
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "y"

上面代碼中,foobar都是Generator函數,在bar裏面調用foo,是不會有效果的。

這個就須要用到yield*語句,用來在一個Generator函數裏面執行另外一個Generator函數。

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  for (let v of foo()) {
    console.log(v);
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"

再來看一個對比的例子。

function* inner() {
  yield 'hello!';
}

function* outer1() {
  yield 'open';
  yield inner();
  yield 'close';
}

var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一個遍歷器對象
gen.next().value // "close"

function* outer2() {
  yield 'open'
  yield* inner()
  yield 'close'
}

var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"

上面例子中,outer2使用了yield*outer1沒使用。結果就是,outer1返回一個遍歷器對象,outer2返回該遍歷器對象的內部值。

從語法角度看,若是yield命令後面跟的是一個遍歷器對象,須要在yield命令後面加上星號,代表它返回的是一個遍歷器對象。這被稱爲yield*語句。

let delegatedIterator = (function* () {
  yield 'Hello!';
  yield 'Bye!';
}());

let delegatingIterator = (function* () {
  yield 'Greetings!';
  yield* delegatedIterator;
  yield 'Ok, bye.';
}());

for(let value of delegatingIterator) {
  console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."

上面代碼中,delegatingIterator是代理者,delegatedIterator是被代理者。因爲yield* delegatedIterator語句獲得的值,是一個遍歷器,因此要用星號表示。運行結果就是使用一個遍歷器,遍歷了多個Generator函數,有遞歸的效果。

yield*語句等同於在Generator函數內部,部署一個for…of循環。

function* concat(iter1, iter2) {
  yield* iter1;
  yield* iter2;
}

// 等同於

function* concat(iter1, iter2) {
  for (var value of iter1) {
    yield value;
  }
  for (var value of iter2) {
    yield value;
  }
}

上面代碼說明,yield*不過是for...of的一種簡寫形式,徹底能夠用後者替代前者。

若是yield*後面跟着一個數組,因爲數組原生支持遍歷器,所以就會遍歷數組成員。

function* gen(){
  yield* ["a", "b", "c"];
}

gen().next() // { value:"a", done:false }

上面代碼中,yield命令後面若是不加星號,返回的是整個數組,加了星號就表示返回的是數組的遍歷器對象。

實際上,任何數據結構只要有Iterator接口,就能夠被yield*遍歷。

let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();

read.next().value // "hello"
read.next().value // "h"

上面代碼中,yield語句返回整個字符串,yield*語句返回單個字符。由於字符串具備Iterator接口,因此被yield*遍歷。

若是被代理的Generator函數有return語句,那麼就能夠向代理它的Generator函數返回數據。

function *foo() {
  yield 2;
  yield 3;
  return "foo";
}

function *bar() {
  yield 1;
  var v = yield *foo();
  console.log( "v: " + v );
  yield 4;
}

var it = bar();

it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}

上面代碼在第四次調用next方法的時候,屏幕上會有輸出,這是由於函數fooreturn語句,向函數bar提供了返回值。

yield*命令能夠很方便地取出嵌套數組的全部成員。

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e

下面是一個稍微複雜的例子,使用yield*語句遍歷徹底二叉樹。

// 下面是二叉樹的構造函數,
// 三個參數分別是左樹、當前節點和右樹
function Tree(left, label, right) {
  this.left = left;
  this.label = label;
  this.right = right;
}

// 下面是中序(inorder)遍歷函數。
// 因爲返回的是一個遍歷器,因此要用generator函數。
// 函數體內採用遞歸算法,因此左樹和右樹要用yield*遍歷
function* inorder(t) {
  if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
  }
}

// 下面生成二叉樹
function make(array) {
  // 判斷是否爲葉節點
  if (array.length == 1) return new Tree(null, array[0], null);
  return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);

// 遍歷二叉樹
var result = [];
for (let node of inorder(tree)) {
  result.push(node);
}

result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']

做爲對象屬性的Generator函數

若是一個對象的屬性是Generator函數,能夠簡寫成下面的形式。

let obj = {
  * myGeneratorMethod() {
    ···
  }
};

上面代碼中,myGeneratorMethod屬性前面有一個星號,表示這個屬性是一個Generator函數。

它的完整形式以下,與上面的寫法是等價的。

let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

構造函數是Generator函數

這一節討論一種特殊狀況:構造函數是Generator函數。

function* F(){
  yield this.x = 2;
  yield this.y = 3;
}

上面代碼中,函數F是一個構造函數,又是一個Generator函數。這時,使用new命令就沒法生成F的實例了,由於F返回的是一個內部指針。

'next' in (new F())
// true

上面代碼中,因爲new F()返回的是一個Iterator對象,具備next方法,因此上面的表達式爲true。

那麼,這個時候怎麼生成對象實例呢?

咱們知道,若是構造函數調用時,沒有使用new命令,那麼內部的this對象,綁定當前構造函數所在的對象(好比window對象)。所以,能夠生成一個空對象,使用bind方法綁定F內部的this。這樣,構造函數調用之後,這個空對象就是F的實例對象了。

var obj = {};
var f = F.bind(obj)();

f.next();
f.next();
f.next();

console.log(obj);
// { x: 2, y: 3 }

上面代碼中,首先是F內部的this對象綁定obj對象,而後調用它,返回一個Iterator對象。這個對象執行三次next方法(由於F內部有兩個yield語句),完成F內部全部代碼的運行。這時,全部內部屬性都綁定在obj對象上了,所以obj對象也就成了F的實例。

Generator函數推導

ES7在數組推導的基礎上,提出了Generator函數推導(Generator comprehension)。

let generator = function* () {
  for (let i = 0; i < 6; i++) {
    yield i;
  }
}

let squared = ( for (n of generator()) n * n );
// 等同於
// let squared = Array.from(generator()).map(n => n * n);

console.log(...squared);
// 0 1 4 9 16 25

「推導」這種語法結構,不只能夠用於數組,ES7將其推廣到了Generator函數。for…of循環會自動調用遍歷器的next方法,將返回值的value屬性做爲數組的一個成員。

Generator函數推導是對數組結構的一種模擬,它的最大優勢是惰性求值,即直到真正用到時纔會求值,這樣能夠保證效率。請看下面的例子。

let bigArray = new Array(100000);
for (let i = 0; i < 100000; i++) {
  bigArray[i] = i;
}

let first = bigArray.map(n => n * n)[0];
console.log(first);

上面例子遍歷一個大數組,可是在真正遍歷以前,這個數組已經生成了,佔用了系統資源。若是改用Generator函數推導,就能避免這一點。下面代碼只在用到時,纔會生成一個大數組。

let bigGenerator = function* () {
  for (let i = 0; i < 100000; i++) {
    yield i;
  }
}

let squared = ( for (n of bigGenerator()) n * n );

console.log(squared.next());

含義

Generator與狀態機

Generator是實現狀態機的最佳結構。好比,下面的clock函數就是一個狀態機。

var ticking = true;
var clock = function() {
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}

上面代碼的clock函數一共有兩種狀態(Tick和Tock),每運行一次,就改變一次狀態。這個函數若是用Generator實現,就是下面這樣。

var clock = function*(_) {
  while (true) {
    yield _;
    console.log('Tick!');
    yield _;
    console.log('Tock!');
  }
};

上面的Generator實現與ES5實現對比,能夠看到少了用來保存狀態的外部變量ticking,這樣就更簡潔,更安全(狀態不會被非法篡改)、更符合函數式編程的思想,在寫法上也更優雅。Generator之因此能夠不用外部變量保存狀態,是由於它自己就包含了一個狀態信息,即目前是否處於暫停態。

Generator與協程

協程(coroutine)是一種程序運行的方式,能夠理解成「協做的線程」或「協做的函數」。協程既能夠用單線程實現,也能夠用多線程實現。前者是一種特殊的子例程,後者是一種特殊的線程。

(1)協程與子例程的差別

傳統的「子例程」(subroutine)採用堆棧式「後進先出」的執行方式,只有當調用的子函數徹底執行完畢,纔會結束執行父函數。協程與其不一樣,多個線程(單線程狀況下,即多個函數)能夠並行執行,可是隻有一個線程(或函數)處於正在運行的狀態,其餘線程(或函數)都處於暫停態(suspended),線程(或函數)之間能夠交換執行權。也就是說,一個線程(或函數)執行到一半,能夠暫停執行,將執行權交給另外一個線程(或函數),等到稍後收回執行權的時候,再恢復執行。這種能夠並行執行、交換執行權的線程(或函數),就稱爲協程。

從實現上看,在內存中,子例程只使用一個棧(stack),而協程是同時存在多個棧,但只有一個棧是在運行狀態,也就是說,協程是以多佔用內存爲代價,實現多任務的並行。

(2)協程與普通線程的差別

不難看出,協程適合用於多任務運行的環境。在這個意義上,它與普通的線程很類似,都有本身的執行上下文、能夠分享全局變量。它們的不一樣之處在於,同一時間能夠有多個線程處於運行狀態,可是運行的協程只能有一個,其餘協程都處於暫停狀態。此外,普通的線程是搶先式的,到底哪一個線程優先獲得資源,必須由運行環境決定,可是協程是合做式的,執行權由協程本身分配。

因爲ECMAScript是單線程語言,只能保持一個調用棧。引入協程之後,每一個任務能夠保持本身的調用棧。這樣作的最大好處,就是拋出錯誤的時候,能夠找到原始的調用棧。不至於像異步操做的回調函數那樣,一旦出錯,原始的調用棧早就結束。

Generator函數是ECMAScript 6對協程的實現,但屬於不徹底實現。Generator函數被稱爲「半協程」(semi-coroutine),意思是隻有Generator函數的調用者,才能將程序的執行權還給Generator函數。若是是徹底執行的協程,任何函數均可以讓暫停的協程繼續執行。

若是將Generator函數看成協程,徹底能夠將多個須要互相協做的任務寫成Generator函數,它們之間使用yield語句交換控制權。

應用

Generator能夠暫停函數執行,返回任意表達式的值。這種特色使得Generator有多種應用場景。

(1)異步操做的同步化表達

Generator函數的暫停執行的效果,意味着能夠把異步操做寫在yield語句裏面,等到調用next方法時再日後執行。這實際上等同於不須要寫回調函數了,由於異步操做的後續操做能夠放在yield語句下面,反正要等到調用next方法時再執行。因此,Generator函數的一個重要實際意義就是用來處理異步操做,改寫回調函數。

function* loadUI() {
  showLoadingScreen();
  yield loadUIDataAsynchronously();
 hideLoadingScreen();
}
var loader = loadUI();
// 加載UI
loader.next()

// 卸載UI
loader.next()

上面代碼表示,第一次調用loadUI函數時,該函數不會執行,僅返回一個遍歷器。下一次對該遍歷器調用next方法,則會顯示Loading界面,而且異步加載數據。等到數據加載完成,再一次使用next方法,則會隱藏Loading界面。能夠看到,這種寫法的好處是全部Loading界面的邏輯,都被封裝在一個函數,循序漸進很是清晰。

Ajax是典型的異步操做,經過Generator函數部署Ajax操做,能夠用同步的方式表達。

function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url) {
  makeAjaxCall(url, function(response){
    it.next(response);
  });
}

var it = main();
it.next();

上面代碼的main函數,就是經過Ajax操做獲取數據。能夠看到,除了多了一個yield,它幾乎與同步操做的寫法徹底同樣。注意,makeAjaxCall函數中的next方法,必須加上response參數,由於yield語句構成的表達式,自己是沒有值的,老是等於undefined。

下面是另外一個例子,經過Generator函數逐行讀取文本文件。

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while(!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}

上面代碼打開文本文件,使用yield語句能夠手動逐行讀取文件。

(2)控制流管理

若是有一個多步操做很是耗時,採用回調函數,可能會寫成下面這樣。

step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

採用Promise改寫上面的代碼。

Q.fcall(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(function (value4) {
    // Do something with value4
  }, function (error) {
    // Handle any error from step1 through step4
  })
  .done();

上面代碼已經把回調函數,改爲了直線執行的形式,可是加入了大量Promise的語法。Generator函數能夠進一步改善代碼運行流程。

function* longRunningTask() {
  try {
    var value1 = yield step1();
    var value2 = yield step2(value1);
    var value3 = yield step3(value2);
    var value4 = yield step4(value3);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

而後,使用一個函數,按次序自動執行全部步驟。

scheduler(longRunningTask());

function scheduler(task) {
  setTimeout(function() {
    var taskObj = task.next(task.value);
    // 若是Generator函數未結束,就繼續調用
    if (!taskObj.done) {
      task.value = taskObj.value
      scheduler(task);
    }
  }, 0);
}

注意,yield語句是同步運行,不是異步運行(不然就失去了取代回調函數的設計目的了)。實際操做中,通常讓yield語句返回Promise對象。

var Q = require('q');

function delay(milliseconds) {
  var deferred = Q.defer();
  setTimeout(deferred.resolve, milliseconds);
  return deferred.promise;
}

function* f(){
  yield delay(100);
};

上面代碼使用Promise的函數庫Q,yield語句返回的就是一個Promise對象。

多個任務按順序一個接一個執行時,yield語句能夠按順序排列。多個任務須要並列執行時(好比只有A任務和B任務都執行完,才能執行C任務),能夠採用數組的寫法。

function* parallelDownloads() {
  let [text1,text2] = yield [
    taskA(),
    taskB()
  ];
  console.log(text1, text2);
}

上面代碼中,yield語句的參數是一個數組,成員就是兩個任務taskA和taskB,只有等這兩個任務都完成了,纔會接着執行下面的語句。

(3)部署iterator接口

利用Generator函數,能夠在任意對象上部署iterator接口。

function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// foo 3
// bar 7

上述代碼中,myObj是一個普通對象,經過iterEntries函數,就有了iterator接口。也就是說,能夠在任意對象上部署next方法。

下面是一個對數組部署Iterator接口的例子,儘管數組原生具備這個接口。

function* makeSimpleGenerator(array){
  var nextIndex = 0;

  while(nextIndex < array.length){
    yield array[nextIndex++];
  }
}

var gen = makeSimpleGenerator(['yo', 'ya']);

gen.next().value // 'yo'
gen.next().value // 'ya'
gen.next().done  // true

(4)做爲數據結構

Generator能夠看做是數據結構,更確切地說,能夠看做是一個數組結構,由於Generator函數能夠返回一系列的值,這意味着它能夠對任意表達式,提供相似數組的接口。

function *doStuff() {
  yield fs.readFile.bind(null, 'hello.txt');
  yield fs.readFile.bind(null, 'world.txt');
  yield fs.readFile.bind(null, 'and-such.txt');
}

上面代碼就是依次返回三個函數,可是因爲使用了Generator函數,致使能夠像處理數組那樣,處理這三個返回的函數。

for (task of doStuff()) {
  // task是一個函數,能夠像回調函數那樣使用它
}

實際上,若是用ES5表達,徹底能夠用數組模擬Generator的這種用法。

function doStuff() {
  return [
    fs.readFile.bind(null, 'hello.txt'),
    fs.readFile.bind(null, 'world.txt'),
    fs.readFile.bind(null, 'and-such.txt')
  ];
}

上面的函數,能夠用如出一轍的for…of循環處理!兩相一比較,就不難看出Generator使得數據或者操做,具有了相似數組的接口。

相關文章
相關標籤/搜索