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

Generator 函數(shù)

簡介

所謂 Generator,有多種理解角度。首先,可以把它理解成一個函數(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 是一個普通函數(shù),但是有兩個特征。一是,function 命令與函數(shù)名之間有一個星號;二是,函數(shù)體內(nèi)部使用 yield 語句,定義遍歷器的每個成員,即不同的內(nèi)部狀態(tài)(yield 語句在英語里的意思就是“產(chǎn)出”)。

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

var hw = helloWorldGenerator();

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

當調(diào)用 Generator 函數(shù)的時候,該函數(shù)并不執(zhí)行,而是返回一個遍歷器(可以理解成暫停執(zhí)行)。以后,每次調(diào)用這個遍歷器的 next 方法,就從函數(shù)體的頭部或者上一次停下來的地方開始執(zhí)行(可以理解成恢復執(zhí)行),直到遇到下一個 yield 語句為止。也就是說,next 方法就是在遍歷 yield 語句定義的內(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ù)開始執(zhí)行,直到遇到第一句 yield 語句為止。next 方法返回一個對象,它的 value 屬性就是當前 yield 語句的值 hello,done 屬性的值 false,表示遍歷還沒有結(jié)束。

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

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

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

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

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


function* gen(){
  // some code
}

var g = gen();

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

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

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

yield 語句與 return 語句有點像,都能返回緊跟在語句后面的那個表達式的值。區(qū)別在于每次遇到 yield,函數(shù)暫停執(zhí)行,下一次再從該位置繼續(xù)向后執(zhí)行,而return語句不具備位置記憶的功能。一個函數(shù)里面,只能執(zhí)行一次(或者說一個)return 語句,但是可以執(zhí)行多次(或者說多個)yield 語句。正常函數(shù)只能返回一個值,因為只能執(zhí)行一次 return;Generator 函數(shù)可以返回一系列的值,因為可以有任意多個 yield。從另一個角度看,也可以說 Generator 生成了一系列的值,這也就是它的名稱的來歷(在英語中,generator 這個詞是“生成器”的意思)。

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


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

var generator = f();

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

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

另外需要注意,yield 語句不能用在普通函數(shù)中,否則會報錯。


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

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

下面是另外一個例子。


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

上面代碼也會產(chǎn)生句法錯誤,因為 forEach 方法的參數(shù)是一個普通函數(shù),但是在里面使用了 yield 語句。一種修改方法是改用 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 語句本身沒有返回值,或者說總是返回 undefined。next 方法可以帶一個參數(shù),該參數(shù)就會被當作上一個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 函數(shù) f,如果 next 方法沒有參數(shù),每次運行到 yield 語句,變量 reset 的值總是 undefined。當 next 方法帶一個參數(shù) true 時,當前的變量 reset 就被重置為這個參數(shù)(即 true),因此 i 會等于 -1,下一輪循環(huán)就會從 -1 開始遞增。

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

再看一個例子。

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 方法的時候不帶參數(shù),導致y的值等于2 * undefined(即 NaN),除以 3 以后還是 NaN,因此返回對象的 value 屬性也等于 NaN。第三次運行 Next 方法的時候不帶參數(shù),所以 z 等于 undefined,返回對象的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 方法時,返回x+1的值 6;第二次調(diào)用 next 方法,將上一次 yield 語句的值設(shè)為 12,因此 y 等于 24,返回y / 3的值 8;第三次調(diào)用 next 方法,將上一次 yield 語句的值設(shè)為 13,因此 z 等于 13,這時 x 等于 5,y 等于 24,所以 return 語句的值等于 42。

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

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

for...of 循環(huán)可以自動遍歷 Generator 函數(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 個 yield 語句的值。這里需要注意,一旦 next 方法的返回對象的done屬性為 true,for...of 循環(huán)就會中止,且不包含該返回對象,所以上面代碼的 return 語句返回的 6,不包括在 for...of 循環(huán)之中。

下面是一個利用 generator 函數(shù)和 for...of 循環(huán),實現(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);
}

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

throw 方法

Generator 函數(shù)還有一個特點,它可以在函數(shù)體外拋出錯誤,然后在函數(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ù)拋出兩個錯誤。第一個錯誤被 Generator 函數(shù)體內(nèi)的 catch 捕獲,然后 Generator 函數(shù)執(zhí)行完成,于是第二個錯誤被函數(shù)體外的 catch 捕獲。

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

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,是因為函數(shù)體外的 catch 語句塊,捕獲了拋出的a錯誤以后,就不會再繼續(xù)執(zhí)行 try語句塊了。

如果遍歷器函數(shù)內(nèi)部沒有部署 try...catch 代碼塊,那么 throw 方法拋出的錯誤,將被外部 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)部,沒有部署 try...catch 代碼塊,所以拋出的錯誤直接被外部 catch 代碼塊捕獲。

如果遍歷器函數(shù)內(nèi)部部署了 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 就結(jié)束了,因為第二次調(diào)用 next 方法時,遍歷器狀態(tài)已經(jīng)變成終止了。但是,如果使用 throw 方法拋出錯誤,不會影響遍歷器狀態(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 命令拋出的錯誤不會影響到遍歷器的狀態(tài),所以兩次執(zhí)行 next 方法,都取到了正確的操作。

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

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ù)可以大大簡化上面的代碼。

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 函數(shù)內(nèi)拋出的錯誤,也可以被函數(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);
}

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

一旦 Generator 執(zhí)行過程中拋出錯誤,就不會再執(zhí)行下去了。如果此后還調(diào)用 next 方法,將返回一個 value 屬性等于 undefined、done 屬性等于 true 的對象,即 JavaScript 引擎認為這個 Generator 已經(jīng)運行結(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('第一次運行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 函數(shù)就已經(jīng)結(jié)束了,不再執(zhí)行下去了。

yield*語句

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

再來看一個對比的例子。

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 沒使用。結(jié)果就是,outer1 返回一個遍歷器,outer2 返回該遍歷器的內(nèi)部值。

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


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

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

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

如果被代理的 Generator 函數(shù)有 return 語句,那么就可以向代理它的 Generator 函數(shù)返回數(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ù) foo 的 return 語句,向函數(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

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


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

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

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

如果一個對象的屬性是 Generator 函數(shù),可以簡寫成下面的形式。

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

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

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

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

Generator 函數(shù)推導

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

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

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

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

上面例子遍歷一個大數(shù)組,但是在真正遍歷之前,這個數(shù)組已經(jīng)生成了,占用了系統(tǒng)資源。如果改用 Generator 函數(shù)推導,就能避免這一點。下面代碼只在用到時,才會生成一個大數(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)機

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


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

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


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

上面的 Generator 實現(xiàn)與 ES5 實現(xiàn)對比,可以看到少了用來保存狀態(tài)的外部變量 ticking,這樣就更簡潔,更安全(狀態(tài)不會被非法篡改)、更符合函數(shù)式編程的思想,在寫法上也更優(yōu)雅。Generator 之所以可以不用外部變量保存狀態(tài),是因為它本身就包含了一個狀態(tài)信息,即目前是否處于暫停態(tài)。

Generator 與協(xié)程

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

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

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

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

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

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

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

Generator 函數(shù)是 ECMAScript 6 對協(xié)程的實現(xiàn),但屬于不完全實現(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ù)當作協(xié)程,完全可以將多個需要互相協(xié)作的任務(wù)寫成 Generator 函數(shù),它們之間使用yield語句交換控制權(quán)。

應(yīng)用

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

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

Generator 函數(shù)的暫停執(zhí)行的效果,意味著可以把異步操作寫在 yield 語句里面,等到調(diào)用 next 方法時再往后執(zhí)行。這實際上等同于不需要寫回調(diào)函數(shù)了,因為異步操作的后續(xù)操作可以放在 yield 語句下面,反正要等到調(diào)用next 方法時再執(zhí)行。所以,Generator 函數(shù)的一個重要實際意義就是用來處理異步操作,改寫回調(diào)函數(shù)。


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

// 卸載UI
loader.next()

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

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

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


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

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

(2)控制流管理

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


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

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


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

然后,使用一個函數(shù),按次序自動執(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 語句是同步運行,不是異步運行(否則就失去了取代回調(diào)函數(shù)的設(shè)計目的了)。實際操作中,一般讓 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 的函數(shù)庫 Q,yield 語句返回的就是一個 Promise 對象。

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


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

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

(3)部署 iterator 接口

利用 Generator 函數(shù),可以在任意對象上部署 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 函數(shù),就有了 iterator 接口。也就是說,可以在任意對象上部署 next 方法。

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


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),更確切地說,可以看作是一個數(shù)組結(jié)構(gòu),因為 Generator 函數(shù)可以返回一系列的值,這意味著它可以對任意表達式,提供類似數(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');
}

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


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

實際上,如果用 ES5 表達,完全可以用數(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ù)組的接口。