鍍金池/ 教程/ HTML/ Generator 函數(shù)
數(shù)組的擴(kuò)展
Class和Module
Set 和 Map 數(shù)據(jù)結(jié)構(gòu)
異步操作
對(duì)象的擴(kuò)展
Generator 函數(shù)
數(shù)值的擴(kuò)展
變量的解構(gòu)賦值
Iterator 和 for...of 循環(huán)
Promise 對(duì)象
參考鏈接
ECMAScript 6簡(jiǎn)介
作者簡(jiǎn)介
字符串的擴(kuò)展
編程風(fēng)格
let 和 const 命令
函數(shù)的擴(kuò)展

Generator 函數(shù)

簡(jiǎn)介

所謂 Generator,有多種理解角度。首先,可以把它理解成一個(gè)函數(shù)的內(nèi)部狀態(tài)的遍歷器,每調(diào)用一次,函數(shù)的內(nèi)部狀態(tài)發(fā)生一次改變(可以理解成發(fā)生某些事件)。ES6 引入 Generator 函數(shù),作用就是可以完全控制函數(shù)的內(nèi)部狀態(tài)的變化,依次遍歷這些狀態(tài)。

在形式上,Generator 是一個(gè)普通函數(shù),但是有兩個(gè)特征。一是,function 命令與函數(shù)名之間有一個(gè)星號(hào);二是,函數(shù)體內(nèi)部使用 yield 語(yǔ)句,定義遍歷器的每個(gè)成員,即不同的內(nèi)部狀態(tài)(yield 語(yǔ)句在英語(yǔ)里的意思就是“產(chǎn)出”)。

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

var hw = helloWorldGenerator();

上面代碼定義了一個(gè) Generator 函數(shù) helloWorldGenerator,它的遍歷器有兩個(gè)成員“hello”和“world”。調(diào)用這個(gè)函數(shù),就會(huì)得到遍歷器。

當(dāng)調(diào)用 Generator 函數(shù)的時(shí)候,該函數(shù)并不執(zhí)行,而是返回一個(gè)遍歷器(可以理解成暫停執(zhí)行)。以后,每次調(diào)用這個(gè)遍歷器的 next 方法,就從函數(shù)體的頭部或者上一次停下來(lái)的地方開(kāi)始執(zhí)行(可以理解成恢復(fù)執(zhí)行),直到遇到下一個(gè) yield 語(yǔ)句為止。也就是說(shuō),next 方法就是在遍歷 yield 語(yǔ)句定義的內(nèi)部狀態(tài)。


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

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

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

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

上面代碼一共調(diào)用了四次 next 方法。

第一次調(diào)用,函數(shù)開(kāi)始執(zhí)行,直到遇到第一句 yield 語(yǔ)句為止。next 方法返回一個(gè)對(duì)象,它的 value 屬性就是當(dāng)前 yield 語(yǔ)句的值 hello,done 屬性的值 false,表示遍歷還沒(méi)有結(jié)束。

第二次調(diào)用,函數(shù)從上次 yield 語(yǔ)句停下的地方,一直執(zhí)行到下一個(gè) yield 語(yǔ)句。next 方法返回的對(duì)象的 value 屬性就是當(dāng)前 yield 語(yǔ)句的值 world,done 屬性的值 false,表示遍歷還沒(méi)有結(jié)束。

第三次調(diào)用,函數(shù)從上次 yield 語(yǔ)句停下的地方,一直執(zhí)行到 return 語(yǔ)句(如果沒(méi)有 return 語(yǔ)句,就執(zhí)行到函數(shù)結(jié)束)。next 方法返回的對(duì)象的 value 屬性,就是緊跟在 return 語(yǔ)句后面的表達(dá)式的值(如果沒(méi)有 return 語(yǔ)句,則 value 屬性的值為 undefined),done 屬性的值 true,表示遍歷已經(jīng)結(jié)束。

第四次調(diào)用,此時(shí)函數(shù)已經(jīng)運(yùn)行完畢,next 方法返回對(duì)象的 value 屬性為 undefined,done 屬性為 true。以后再調(diào)用 next 方法,返回的都是這個(gè)值。

總結(jié)一下,Generator 函數(shù)使用 iterator 接口,每次調(diào)用 next 方法的返回值,就是一個(gè)標(biāo)準(zhǔn)的 iterator 返回值:有著 value 和 done 兩個(gè)屬性的對(duì)象。其中,value 是 yield 語(yǔ)句后面那個(gè)表達(dá)式的值,done 是一個(gè)布爾值,表示是否遍歷結(jié)束。

上一章說(shuō)過(guò),任意一個(gè)對(duì)象的 Symbol.iterator 屬性,等于該對(duì)象的遍歷器函數(shù),即調(diào)用該函數(shù)會(huì)返回該對(duì)象的一個(gè)遍歷器。遍歷器本身也是一個(gè)對(duì)象,它的 Symbol.iterator 屬性執(zhí)行后,返回自身。


function* gen(){
  // some code
}

var g = gen();

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

上面代碼中,gen 是一個(gè) Generator 函數(shù),調(diào)用它會(huì)生成一個(gè)遍歷器 g。遍歷器 g 的 Symbol.iterator 屬性是一個(gè)遍歷器函數(shù),執(zhí)行后返回它自己。

由于 Generator 函數(shù)返回的遍歷器,只有調(diào)用 next 方法才會(huì)遍歷下一個(gè)成員,所以其實(shí)提供了一種可以暫停執(zhí)行的函數(shù)。yield 語(yǔ)句就是暫停標(biāo)志,next 方法遇到 yield,就會(huì)暫停執(zhí)行后面的操作,并將緊跟在 yield 后面的那個(gè)表達(dá)式的值,作為返回對(duì)象的 value 屬性的值。當(dāng)下一次調(diào)用next方法時(shí),再繼續(xù)往下執(zhí)行,直到遇到下一個(gè) yield 語(yǔ)句。如果沒(méi)有再遇到新的 yield 語(yǔ)句,就一直運(yùn)行到函數(shù)結(jié)束,將 return 語(yǔ)句后面的表達(dá)式的值,作為 value 屬性的值,如果該函數(shù)沒(méi)有 return 語(yǔ)句,則 value 屬性的值為 undefined。另一方面,由于 yield 后面的表達(dá)式,直到調(diào)用 next 方法時(shí)才會(huì)執(zhí)行,因此等于為 JavaScript 提供了手動(dòng)的“惰性求值”(Lazy Evaluation)的語(yǔ)法功能。

yield 語(yǔ)句與 return 語(yǔ)句有點(diǎn)像,都能返回緊跟在語(yǔ)句后面的那個(gè)表達(dá)式的值。區(qū)別在于每次遇到 yield,函數(shù)暫停執(zhí)行,下一次再?gòu)脑撐恢美^續(xù)向后執(zhí)行,而return語(yǔ)句不具備位置記憶的功能。一個(gè)函數(shù)里面,只能執(zhí)行一次(或者說(shuō)一個(gè))return 語(yǔ)句,但是可以執(zhí)行多次(或者說(shuō)多個(gè))yield 語(yǔ)句。正常函數(shù)只能返回一個(gè)值,因?yàn)橹荒軋?zhí)行一次 return;Generator 函數(shù)可以返回一系列的值,因?yàn)榭梢杂腥我舛鄠€(gè) yield。從另一個(gè)角度看,也可以說(shuō) Generator 生成了一系列的值,這也就是它的名稱的來(lái)歷(在英語(yǔ)中,generator 這個(gè)詞是“生成器”的意思)。

Generator 函數(shù)可以不用 yield 語(yǔ)句,這時(shí)就變成了一個(gè)單純的暫緩執(zhí)行函數(shù)。


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

var generator = f();

setTimeout(function () {
  generator.next()
}, 2000);

上面代碼中,函數(shù)f如果是普通函數(shù),在為變量 generator 賦值時(shí)就會(huì)執(zhí)行。但是,函數(shù)f是一個(gè) Generator 函數(shù),就變成只有調(diào)用 next 方法時(shí),函數(shù) f 才會(huì)執(zhí)行。

另外需要注意,yield 語(yǔ)句不能用在普通函數(shù)中,否則會(huì)報(bào)錯(cuò)。


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

上面代碼在一個(gè)普通函數(shù)中使用 yield 語(yǔ)句,結(jié)果產(chǎn)生一個(gè)句法錯(cuò)誤。

下面是另外一個(gè)例子。


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);
}

上面代碼也會(huì)產(chǎn)生句法錯(cuò)誤,因?yàn)?forEach 方法的參數(shù)是一個(gè)普通函數(shù),但是在里面使用了 yield 語(yǔ)句。一種修改方法是改用 for 循環(huán)。

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

next 方法的參數(shù)

yield 語(yǔ)句本身沒(méi)有返回值,或者說(shuō)總是返回 undefined。next 方法可以帶一個(gè)參數(shù),該參數(shù)就會(huì)被當(dāng)作上一個(gè)yield 語(yǔ)句的返回值。

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 }

上面代碼先定義了一個(gè)可以無(wú)限運(yùn)行的 Generator 函數(shù) f,如果 next 方法沒(méi)有參數(shù),每次運(yùn)行到 yield 語(yǔ)句,變量 reset 的值總是 undefined。當(dāng) next 方法帶一個(gè)參數(shù) true 時(shí),當(dāng)前的變量 reset 就被重置為這個(gè)參數(shù)(即 true),因此 i 會(huì)等于 -1,下一輪循環(huán)就會(huì)從 -1 開(kāi)始遞增。

這個(gè)功能有很重要的語(yǔ)法意義。Generator 函數(shù)從暫停狀態(tài)到恢復(fù)運(yùn)行,它的上下文狀態(tài)(context)是不變的。通過(guò) next 方法的參數(shù),就有辦法在 Generator 函數(shù)開(kāi)始運(yùn)行之后,繼續(xù)向函數(shù)體內(nèi)部注入值。也就是說(shuō),可以在 Generator 函數(shù)運(yùn)行的不同階段,從外部向內(nèi)部注入不同的值,從而調(diào)整函數(shù)行為。

再看一個(gè)例子。

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}

上面代碼中,第二次運(yùn)行 next 方法的時(shí)候不帶參數(shù),導(dǎo)致y的值等于2 * undefined(即 NaN),除以 3 以后還是 NaN,因此返回對(duì)象的 value 屬性也等于 NaN。第三次運(yùn)行 Next 方法的時(shí)候不帶參數(shù),所以 z 等于 undefined,返回對(duì)象的value屬性等于5 + NaN + undefined,即 NaN。

如果向 next 方法提供參數(shù),返回結(jié)果就完全不一樣了。

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 }

上面代碼第一次調(diào)用 next 方法時(shí),返回x+1的值 6;第二次調(diào)用 next 方法,將上一次 yield 語(yǔ)句的值設(shè)為 12,因此 y 等于 24,返回y / 3的值 8;第三次調(diào)用 next 方法,將上一次 yield 語(yǔ)句的值設(shè)為 13,因此 z 等于 13,這時(shí) x 等于 5,y 等于 24,所以 return 語(yǔ)句的值等于 42。

注意,由于 next 方法的參數(shù)表示上一個(gè) yield 語(yǔ)句的返回值,所以第一次使用 next 方法時(shí),不能帶有參數(shù)。V8 引擎直接忽略第一次使用 next 方法時(shí)的參數(shù),只有從第二次使用 next 方法開(kāi)始,參數(shù)才是有效的。

for...of 循環(huán)

for...of 循環(huán)可以自動(dòng)遍歷 Generator 函數(shù),且此時(shí)不再需要調(diào)用 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 循環(huán),依次顯示 5 個(gè) yield 語(yǔ)句的值。這里需要注意,一旦 next 方法的返回對(duì)象的done屬性為 true,for...of 循環(huán)就會(huì)中止,且不包含該返回對(duì)象,所以上面代碼的 return 語(yǔ)句返回的 6,不包括在 for...of 循環(huán)之中。

下面是一個(gè)利用 generator 函數(shù)和 for...of 循環(huán),實(shí)現(xiàn)斐波那契數(shù)列的例子。


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);
}

從上面代碼可見(jiàn),使用 for...of 語(yǔ)句時(shí)不需要使用 next 方法。

throw 方法

Generator 函數(shù)還有一個(gè)特點(diǎn),它可以在函數(shù)體外拋出錯(cuò)誤,然后在函數(shù)體內(nèi)捕獲。

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

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

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

上面代碼中,遍歷器i連續(xù)拋出兩個(gè)錯(cuò)誤。第一個(gè)錯(cuò)誤被 Generator 函數(shù)體內(nèi)的 catch 捕獲,然后 Generator 函數(shù)執(zhí)行完成,于是第二個(gè)錯(cuò)誤被函數(shù)體外的 catch 捕獲。

注意,上面代碼的錯(cuò)誤,是用遍歷器的 throw 方法拋出的,而不是用 throw 命令拋出的。后者只能被函數(shù)體外的 catch 語(yǔ)句捕獲。

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

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

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

上面代碼之所以只捕獲了 a,是因?yàn)楹瘮?shù)體外的 catch 語(yǔ)句塊,捕獲了拋出的a錯(cuò)誤以后,就不會(huì)再繼續(xù)執(zhí)行 try語(yǔ)句塊了。

如果遍歷器函數(shù)內(nèi)部沒(méi)有部署 try...catch 代碼塊,那么 throw 方法拋出的錯(cuò)誤,將被外部 try...catch 代碼塊捕獲。

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

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

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

上面代碼中,遍歷器函數(shù) g 內(nèi)部,沒(méi)有部署 try...catch 代碼塊,所以拋出的錯(cuò)誤直接被外部 catch 代碼塊捕獲。

如果遍歷器函數(shù)內(nèi)部部署了 try...catch 代碼塊,那么遍歷器的 throw 方法拋出的錯(cuò)誤,不影響下一次遍歷,否則遍歷直接終止。

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 就結(jié)束了,因?yàn)榈诙握{(diào)用 next 方法時(shí),遍歷器狀態(tài)已經(jīng)變成終止了。但是,如果使用 throw 方法拋出錯(cuò)誤,不會(huì)影響遍歷器狀態(tài)。

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 命令拋出的錯(cuò)誤不會(huì)影響到遍歷器的狀態(tài),所以兩次執(zhí)行 next 方法,都取到了正確的操作。

這種函數(shù)體內(nèi)捕獲錯(cuò)誤的機(jī)制,大大方便了對(duì)錯(cuò)誤的處理。如果使用回調(diào)函數(shù)的寫(xiě)法,想要捕獲多個(gè)錯(cuò)誤,就不得不為每個(gè)函數(shù)寫(xiě)一個(gè)錯(cuò)誤處理語(yǔ)句。

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 函數(shù)可以大大簡(jiǎn)化上面的代碼。

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);
}

反過(guò)來(lái),Generator 函數(shù)內(nèi)拋出的錯(cuò)誤,也可以被函數(shù)體外的 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);
}

上面代碼中,第二個(gè) next 方法向函數(shù)體內(nèi)傳入一個(gè)參數(shù) 42,數(shù)值是沒(méi)有 toUpperCase 方法的,所以會(huì)拋出一個(gè) TypeError 錯(cuò)誤,被函數(shù)體外的 catch 捕獲。

一旦 Generator 執(zhí)行過(guò)程中拋出錯(cuò)誤,就不會(huì)再執(zhí)行下去了。如果此后還調(diào)用 next 方法,將返回一個(gè) value 屬性等于 undefined、done 屬性等于 true 的對(duì)象,即 JavaScript 引擎認(rèn)為這個(gè) Generator 已經(jīng)運(yùn)行結(jié)束了。

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();
    console.log('第一次運(yùn)行next方法', v);
  } catch (err) {
    console.log('捕捉錯(cuò)誤', v);
  }
  try {
    v = generator.next();
    console.log('第二次運(yùn)行next方法', v);
  } catch (err) {
    console.log('捕捉錯(cuò)誤', v);
  }
  try {
    v = generator.next();
    console.log('第三次運(yùn)行next方法', v);
  } catch (err) {
    console.log('捕捉錯(cuò)誤', v);
  }
  console.log('caller done');
}

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

上面代碼一共三次運(yùn)行 next 方法,第二次運(yùn)行的時(shí)候會(huì)拋出錯(cuò)誤,然后第三次運(yùn)行的時(shí)候,Generator 函數(shù)就已經(jīng)結(jié)束了,不再執(zhí)行下去了。

yield*語(yǔ)句

如果 yield 命令后面跟的是一個(gè)遍歷器,需要在 yield 命令后面加上星號(hào),表明它返回的是一個(gè)遍歷器。這被稱為 yield*語(yǔ)句。


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語(yǔ)句得到的值,是一個(gè)遍歷器,所以要用星號(hào)表示。運(yùn)行結(jié)果就是使用一個(gè)遍歷器,遍歷了多個(gè) Genertor 函數(shù),有遞歸的效果。

再來(lái)看一個(gè)對(duì)比的例子。

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

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

var gen = outer1()
gen.next() // -> 'open'
gen.next() // -> a generator
gen.next() // -> 'close'

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

var gen = outer2()
gen.next() // -> 'open'
gen.next() // -> 'hello!'
gen.next() // -> 'close'

上面例子中,outer2 使用了yield*,outer1 沒(méi)使用。結(jié)果就是,outer1 返回一個(gè)遍歷器,outer2 返回該遍歷器的內(nèi)部值。

如果yield*后面跟著一個(gè)數(shù)組,由于數(shù)組原生支持遍歷器,因此就會(huì)遍歷數(shù)組成員。


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

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

上面代碼中,yield 命令后面如果不加星號(hào),返回的是整個(gè)數(shù)組,加了星號(hào)就表示返回的是數(shù)組的遍歷器。

如果被代理的 Generator 函數(shù)有 return 語(yǔ)句,那么就可以向代理它的 Generator 函數(shù)返回?cái)?shù)據(jù)。


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(); //
it.next(); //
it.next(); //
it.next(); // "v: foo"
it.next(); //

上面代碼在第四次調(diào)用 next 方法的時(shí)候,屏幕上會(huì)有輸出,這是因?yàn)楹瘮?shù) foo 的 return 語(yǔ)句,向函數(shù) bar 提供了返回值。

yield*命令可以很方便地取出嵌套數(shù)組的所有成員。


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

下面是一個(gè)稍微復(fù)雜的例子,使用 yield* 語(yǔ)句遍歷完全二叉樹(shù)。


// 下面是二叉樹(shù)的構(gòu)造函數(shù),
// 三個(gè)參數(shù)分別是左樹(shù)、當(dāng)前節(jié)點(diǎn)和右樹(shù)
function Tree(left, label, right) {
  this.left = left;
  this.label = label;
  this.right = right;
}

// 下面是中序(inorder)遍歷函數(shù)。
// 由于返回的是一個(gè)遍歷器,所以要用generator函數(shù)。
// 函數(shù)體內(nèi)采用遞歸算法,所以左樹(shù)和右樹(shù)要用yield*遍歷
function* inorder(t) {
  if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
  }
}

// 下面生成二叉樹(shù)
function make(array) {
  // 判斷是否為葉節(jié)點(diǎn)
  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']]]);

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

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

作為對(duì)象屬性的 Generator 函數(shù)

如果一個(gè)對(duì)象的屬性是 Generator 函數(shù),可以簡(jiǎn)寫(xiě)成下面的形式。

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

上面代碼中,myGeneratorMethod 屬性前面有一個(gè)星號(hào),表示這個(gè)屬性是一個(gè) Generator 函數(shù)。

它的完整形式如下,與上面的寫(xiě)法是等價(jià)的。

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

Generator 函數(shù)推導(dǎo)

ES7 在數(shù)組推導(dǎo)的基礎(chǔ)上,提出了 Generator 函數(shù)推導(dǎo)(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

“推導(dǎo)”這種語(yǔ)法結(jié)構(gòu),在 ES6 只能用于數(shù)組,ES7 將其推廣到了 Generator 函數(shù)。for...of 循環(huán)會(huì)自動(dòng)調(diào)用遍歷器的 next 方法,將返回值的 value 屬性作為數(shù)組的一個(gè)成員。

Generator 函數(shù)推導(dǎo)是對(duì)數(shù)組結(jié)構(gòu)的一種模擬,它的最大優(yōu)點(diǎn)是惰性求值,即直到真正用到時(shí)才會(huì)求值,這樣可以保證效率。請(qǐng)看下面的例子。

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);

上面例子遍歷一個(gè)大數(shù)組,但是在真正遍歷之前,這個(gè)數(shù)組已經(jīng)生成了,占用了系統(tǒng)資源。如果改用 Generator 函數(shù)推導(dǎo),就能避免這一點(diǎn)。下面代碼只在用到時(shí),才會(huì)生成一個(gè)大數(shù)組。

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 與狀態(tài)機(jī)

Generator 是實(shí)現(xiàn)狀態(tài)機(jī)的最佳結(jié)構(gòu)。比如,下面的 clock 函數(shù)就是一個(gè)狀態(tài)機(jī)。


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

上面代碼的 clock 函數(shù)一共有兩種狀態(tài)(Tick 和 Tock),每運(yùn)行一次,就改變一次狀態(tài)。這個(gè)函數(shù)如果用 Generator 實(shí)現(xiàn),就是下面這樣。


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

上面的 Generator 實(shí)現(xiàn)與 ES5 實(shí)現(xiàn)對(duì)比,可以看到少了用來(lái)保存狀態(tài)的外部變量 ticking,這樣就更簡(jiǎn)潔,更安全(狀態(tài)不會(huì)被非法篡改)、更符合函數(shù)式編程的思想,在寫(xiě)法上也更優(yōu)雅。Generator 之所以可以不用外部變量保存狀態(tài),是因?yàn)樗旧砭桶艘粋€(gè)狀態(tài)信息,即目前是否處于暫停態(tài)。

Generator 與協(xié)程

協(xié)程(coroutine)是一種程序運(yùn)行的方式,可以理解成“協(xié)作的線程”或“協(xié)作的函數(shù)”。協(xié)程既可以用單線程實(shí)現(xiàn),也可以用多線程實(shí)現(xiàn)。前者是一種特殊的子例程,后者是一種特殊的線程。

(1)協(xié)程與子例程的差異

傳統(tǒng)的“子例程”(subroutine)采用堆棧式“后進(jìn)先出”的執(zhí)行方式,只有當(dāng)調(diào)用的子函數(shù)完全執(zhí)行完畢,才會(huì)結(jié)束執(zhí)行父函數(shù)。協(xié)程與其不同,多個(gè)線程(單線程情況下,即多個(gè)函數(shù))可以并行執(zhí)行,但是只有一個(gè)線程(或函數(shù))處于正在運(yùn)行的狀態(tài),其他線程(或函數(shù))都處于暫停態(tài)(suspended),線程(或函數(shù))之間可以交換執(zhí)行權(quán)。也就是說(shuō),一個(gè)線程(或函數(shù))執(zhí)行到一半,可以暫停執(zhí)行,將執(zhí)行權(quán)交給另一個(gè)線程(或函數(shù)),等到稍后收回執(zhí)行權(quán)的時(shí)候,再恢復(fù)執(zhí)行。這種可以并行執(zhí)行、交換執(zhí)行權(quán)的線程(或函數(shù)),就稱為協(xié)程。

從實(shí)現(xiàn)上看,在內(nèi)存中,子例程只使用一個(gè)棧(stack),而協(xié)程是同時(shí)存在多個(gè)棧,但只有一個(gè)棧是在運(yùn)行狀態(tài),也就是說(shuō),協(xié)程是以多占用內(nèi)存為代價(jià),實(shí)現(xiàn)多任務(wù)的并行。

(2)協(xié)程與普通線程的差異

不難看出,協(xié)程適合用于多任務(wù)運(yùn)行的環(huán)境。在這個(gè)意義上,它與普通的線程很相似,都有自己的執(zhí)行上下文、可以分享全局變量。它們的不同之處在于,同一時(shí)間可以有多個(gè)線程處于運(yùn)行狀態(tài),但是運(yùn)行的協(xié)程只能有一個(gè),其他協(xié)程都處于暫停狀態(tài)。此外,普通的線程是搶先式的,到底哪個(gè)線程優(yōu)先得到資源,必須由運(yùn)行環(huán)境決定,但是協(xié)程是合作式的,執(zhí)行權(quán)由協(xié)程自己分配。

由于 ECMAScript 是單線程語(yǔ)言,只能保持一個(gè)調(diào)用棧。引入?yún)f(xié)程以后,每個(gè)任務(wù)可以保持自己的調(diào)用棧。這樣做的最大好處,就是拋出錯(cuò)誤的時(shí)候,可以找到原始的調(diào)用棧。不至于像異步操作的回調(diào)函數(shù)那樣,一旦出錯(cuò),原始的調(diào)用棧早就結(jié)束。

Generator 函數(shù)是 ECMAScript 6 對(duì)協(xié)程的實(shí)現(xiàn),但屬于不完全實(shí)現(xiàn)。Generator 函數(shù)被稱為“半?yún)f(xié)程”(semi-coroutine),意思是只有 Generator 函數(shù)的調(diào)用者,才能將程序的執(zhí)行權(quán)還給 Generator 函數(shù)。如果是完全執(zhí)行的協(xié)程,任何函數(shù)都可以讓暫停的協(xié)程繼續(xù)執(zhí)行。

如果將 Generator 函數(shù)當(dāng)作協(xié)程,完全可以將多個(gè)需要互相協(xié)作的任務(wù)寫(xiě)成 Generator 函數(shù),它們之間使用yield語(yǔ)句交換控制權(quán)。

應(yīng)用

Generator 可以暫停函數(shù)執(zhí)行,返回任意表達(dá)式的值。這種特點(diǎn)使得 Generator 有多種應(yīng)用場(chǎng)景。

(1)異步操作的同步化表達(dá)

Generator 函數(shù)的暫停執(zhí)行的效果,意味著可以把異步操作寫(xiě)在 yield 語(yǔ)句里面,等到調(diào)用 next 方法時(shí)再往后執(zhí)行。這實(shí)際上等同于不需要寫(xiě)回調(diào)函數(shù)了,因?yàn)楫惒讲僮鞯暮罄m(xù)操作可以放在 yield 語(yǔ)句下面,反正要等到調(diào)用next 方法時(shí)再執(zhí)行。所以,Generator 函數(shù)的一個(gè)重要實(shí)際意義就是用來(lái)處理異步操作,改寫(xiě)回調(diào)函數(shù)。


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

// 卸載UI
loader.next()

上面代碼表示,第一次調(diào)用 loadUI 函數(shù)時(shí),該函數(shù)不會(huì)執(zhí)行,僅返回一個(gè)遍歷器。下一次對(duì)該遍歷器調(diào)用 next 方法,則會(huì)顯示 Loading 界面,并且異步加載數(shù)據(jù)。等到數(shù)據(jù)加載完成,再一次使用 next 方法,則會(huì)隱藏Loading 界面??梢钥吹?,這種寫(xiě)法的好處是所有 Loading 界面的邏輯,都被封裝在一個(gè)函數(shù),按部就班非常清晰。

Ajax 是典型的異步操作,通過(guò) Generator 函數(shù)部署 Ajax 操作,可以用同步的方式表達(dá)。


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 函數(shù),就是通過(guò) Ajax 操作獲取數(shù)據(jù)??梢钥吹?,除了多了一個(gè) yield,它幾乎與同步操作的寫(xiě)法完全一樣。注意,makeAjaxCall 函數(shù)中的 next 方法,必須加上 response 參數(shù),因?yàn)?yield 語(yǔ)句構(gòu)成的表達(dá)式,本身是沒(méi)有值的,總是等于 undefined。

下面是另一個(gè)例子,通過(guò) Generator 函數(shù)逐行讀取文本文件。


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

上面代碼打開(kāi)文本文件,使用 yield 語(yǔ)句可以手動(dòng)逐行讀取文件。

(2)控制流管理

如果有一個(gè)多步操作非常耗時(shí),采用回調(diào)函數(shù),可能會(huì)寫(xiě)成下面這樣。


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

采用 Promise 改寫(xiě)上面的代碼。


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();

上面代碼已經(jīng)把回調(diào)函數(shù),改成了直線執(zhí)行的形式,但是加入了大量 Promise 的語(yǔ)法。Generator 函數(shù)可以進(jìn)一步改善代碼運(yùn)行流程。


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
  }
}

然后,使用一個(gè)函數(shù),按次序自動(dòng)執(zhí)行所有步驟。


scheduler(longRunningTask());

function scheduler(task) {
  setTimeout(function() {
    var taskObj = task.next(task.value);
    // 如果Generator函數(shù)未結(jié)束,就繼續(xù)調(diào)用
    if (!taskObj.done) {
      task.value = taskObj.value
      scheduler(task);
    }
  }, 0);
}

注意,yield 語(yǔ)句是同步運(yùn)行,不是異步運(yùn)行(否則就失去了取代回調(diào)函數(shù)的設(shè)計(jì)目的了)。實(shí)際操作中,一般讓 yield 語(yǔ)句返回 Promise 對(duì)象。


var Q = require('q');

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

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

上面代碼使用 Promise 的函數(shù)庫(kù) Q,yield 語(yǔ)句返回的就是一個(gè) Promise 對(duì)象。

多個(gè)任務(wù)按順序一個(gè)接一個(gè)執(zhí)行時(shí),yield 語(yǔ)句可以按順序排列。多個(gè)任務(wù)需要并列執(zhí)行時(shí)(比如只有 A 任務(wù)和 B 任務(wù)都執(zhí)行完,才能執(zhí)行 C 任務(wù)),可以采用數(shù)組的寫(xiě)法。


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

上面代碼中,yield 語(yǔ)句的參數(shù)是一個(gè)數(shù)組,成員就是兩個(gè)任務(wù) taskA 和 taskB,只有等這兩個(gè)任務(wù)都完成了,才會(huì)接著執(zhí)行下面的語(yǔ)句。

(3)部署 iterator 接口

利用 Generator 函數(shù),可以在任意對(duì)象上部署 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 是一個(gè)普通對(duì)象,通過(guò) iterEntries 函數(shù),就有了 iterator 接口。也就是說(shuō),可以在任意對(duì)象上部署 next 方法。

下面是一個(gè)對(duì)數(shù)組部署 Iterator 接口的例子,盡管數(shù)組原生具有這個(gè)接口。


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)作為數(shù)據(jù)結(jié)構(gòu)

Generator 可以看作是數(shù)據(jù)結(jié)構(gòu),更確切地說(shuō),可以看作是一個(gè)數(shù)組結(jié)構(gòu),因?yàn)?Generator 函數(shù)可以返回一系列的值,這意味著它可以對(duì)任意表達(dá)式,提供類似數(shù)組的接口。


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

上面代碼就是依次返回三個(gè)函數(shù),但是由于使用了 Generator 函數(shù),導(dǎo)致可以像處理數(shù)組那樣,處理這三個(gè)返回的函數(shù)。


for (task of doStuff()) {
  // task是一個(gè)函數(shù),可以像回調(diào)函數(shù)那樣使用它
}

實(shí)際上,如果用 ES5 表達(dá),完全可以用數(shù)組模擬 Generator 的這種用法。


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

上面的函數(shù),可以用一模一樣的 for...of 循環(huán)處理!兩相一比較,就不難看出 Generator 使得數(shù)據(jù)或者操作,具備了類似數(shù)組的接口。