Generator生成器

Generator 函數(shù)的語法

簡介

基本概念

Generator 函數(shù)是 ES6 提供的一種異步編程解決方案条舔,語法行為與傳統(tǒng)函數(shù)完全不同期虾。
Generator 函數(shù)有多種理解角度。語法上,首先可以把它理解成,Generator 函數(shù)是一個狀態(tài)機,封裝了多個內(nèi)部狀態(tài)秉扑。
執(zhí)行 Generator 函數(shù)會返回一個遍歷器對象,也就是說调限,Generator 函數(shù)除了狀態(tài)機舟陆,還是一個遍歷器對象生成函數(shù)。返回的遍歷器對象旧噪,可以依次遍歷 Generator 函數(shù)內(nèi)部的每一個狀態(tài)吨娜。

形式上,Generator 函數(shù)是一個普通函數(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妙色,它內(nèi)部有兩個yield表達式(helloworld)桅滋,即該函數(shù)有三個狀態(tài):hello,world 和 return 語句(結束執(zhí)行)身辨。

然后丐谋,Generator 函數(shù)的調(diào)用方法與普通函數(shù)一樣,也是在函數(shù)名后面加上一對圓括號煌珊。不同的是号俐,調(diào)用 Generator 函數(shù)后,該函數(shù)并不執(zhí)行定庵,返回的也不是函數(shù)運行結果吏饿,而是一個指向內(nèi)部狀態(tài)的指針對象,也就是上一章介紹的遍歷器對象(Iterator Object)蔬浙。

下一步猪落,必須調(diào)用遍歷器對象的next方法,使得指針移向下一個狀態(tài)畴博。也就是說笨忌,每次調(diào)用next方法,內(nèi)部指針就從函數(shù)頭部或上一次停下來的地方開始執(zhí)行绎晃,直到遇到下一個yield表達式(或return語句)為止蜜唾。換言之杂曲,Generator 函數(shù)是分段執(zhí)行的庶艾,yield表達式是暫停執(zhí)行的標記,而next方法可以恢復執(zhí)行擎勘。

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)用,Generator 函數(shù)開始執(zhí)行棚饵,直到遇到第一個yield表達式為止煤裙。next方法返回一個對象,它的value屬性就是當前yield表達式的值hello噪漾,done屬性的值false硼砰,表示遍歷還沒有結束。

第二次調(diào)用欣硼,Generator 函數(shù)從上次yield表達式停下的地方题翰,一直執(zhí)行到下一個yield表達式。next方法返回的對象的value屬性就是當前yield表達式的值worlddone屬性的值false豹障,表示遍歷還沒有結束冯事。

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

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

總結一下,調(diào)用 Generator 函數(shù)咆疗,返回一個遍歷器對象漓帚,代表 Generator 函數(shù)的內(nèi)部指針。以后午磁,每次調(diào)用遍歷器對象的next方法尝抖,就會返回一個有著valuedone兩個屬性的對象。value屬性表示當前的內(nèi)部狀態(tài)的值迅皇,是yield表達式后面那個表達式的值昧辽;done屬性是一個布爾值,表示是否遍歷結束登颓。

ES6 沒有規(guī)定搅荞,function關鍵字與函數(shù)名之間的星號,寫在哪個位置框咙。這導致下面的寫法都能通過咕痛。

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }

由于 Generator 函數(shù)仍然是普通函數(shù),所以一般的寫法是上面的第三種喇嘱,即星號緊跟在function關鍵字后面茉贡。本書也采用這種寫法。

yield 表達式

由于 Generator 函數(shù)返回的遍歷器對象者铜,只有調(diào)用next方法才會遍歷下一個內(nèi)部狀態(tài)腔丧,所以其實提供了一種可以暫停執(zhí)行的函數(shù)构蹬。yield表達式就是暫停標志。

遍歷器對象的next方法的運行邏輯如下悔据。

(1)遇到yield表達式庄敛,就暫停執(zhí)行后面的操作,并將緊跟在yield后面的那個表達式的值科汗,作為返回的對象的value屬性值藻烤。

(2)下一次調(diào)用next方法時,再繼續(xù)往下執(zhí)行头滔,直到遇到下一個yield表達式怖亭。

(3)如果沒有再遇到新的yield表達式,就一直運行到函數(shù)結束坤检,直到return語句為止兴猩,并將return語句后面的表達式的值,作為返回的對象的value屬性值早歇。

(4)如果該函數(shù)沒有return語句倾芝,則返回的對象的value屬性值為undefined

需要注意的是箭跳,yield表達式后面的表達式晨另,只有當調(diào)用next方法、內(nèi)部指針指向該語句時才會執(zhí)行谱姓,因此等于為 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能借尿。

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

上面代碼中,yield后面的表達式123 + 456屉来,不會立即求值路翻,只會在next方法將指針移到這一句時,才會求值茄靠。

yield表達式與return語句既有相似之處茂契,也有區(qū)別。相似之處在于嘹黔,都能返回緊跟在語句后面的那個表達式的值账嚎。區(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表達式只能用在 Generator 函數(shù)里面了赌,用在其他地方都會報錯。

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

上面代碼在一個普通函數(shù)中使用yield表達式玄糟,結果產(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表達式(這個函數(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

另外,yield表達式如果用在另一個表達式之中贰军,必須放在圓括號里面玻蝌。

function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

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

yield表達式用作函數(shù)參數(shù)或放在賦值表達式的右邊,可以不加括號词疼。

function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}

與 Iterator 接口的關系

上一章說過俯树,任意一個對象的Symbol.iterator方法,等于該對象的遍歷器生成函數(shù)贰盗,調(diào)用該函數(shù)會返回該對象的一個遍歷器對象许饿。

由于 Generator 函數(shù)就是遍歷器生成函數(shù),因此可以把 Generator 賦值給對象的Symbol.iterator屬性舵盈,從而使得該對象具有 Iterator 接口陋率。

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]

上面代碼中球化,Generator 函數(shù)賦值給Symbol.iterator屬性,從而使得myIterable對象具有了 Iterator 接口瓦糟,可以被...運算符遍歷了筒愚。

Generator 函數(shù)執(zhí)行后,返回一個遍歷器對象菩浙。該對象本身也具有Symbol.iterator屬性锨能,執(zhí)行后返回自身。

function* gen(){
  // some code
}

var g = gen();

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

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

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: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方法的時候不帶參數(shù)挽荠,導致 y 的值等于2 * undefined(即NaN)克胳,除以 3 以后還是NaN,因此返回對象的value屬性也等于NaN圈匆。第三次運行Next方法的時候不帶參數(shù)漠另,所以z等于undefined,返回對象的value屬性等于5 + NaN + undefined跃赚,即NaN笆搓。

如果向next方法提供參數(shù),返回結果就完全不一樣了纬傲。上面代碼第一次調(diào)用bnext方法時满败,返回x+1的值6;第二次調(diào)用next方法叹括,將上一次yield表達式的值設為12算墨,因此y等于24,返回y / 3的值8汁雷;第三次調(diào)用next方法净嘀,將上一次yield表達式的值設為13,因此z等于13侠讯,這時x等于5挖藏,y等于24,所以return語句的值等于42厢漩。

注意膜眠,由于next方法的參數(shù)表示上一個yield表達式的返回值,所以在第一次使用next方法時溜嗜,傳遞參數(shù)是無效的柴底。V8 引擎直接忽略第一次使用next方法時的參數(shù),只有從第二次使用next方法開始粱胜,參數(shù)才是有效的柄驻。從語義上講,第一個next方法用來啟動遍歷器對象焙压,所以不用帶有參數(shù)鸿脓。

再看一個通過next方法的參數(shù),向 Generator 函數(shù)內(nèi)部輸入值的例子涯曲。

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 函數(shù)輸入值,然后打印出來幻件。

如果想要第一次調(diào)用next方法時拨黔,就能夠輸入值,可以在 Generator 函數(shù)外面再包一層绰沥。

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 函數(shù)如果不用wrapper先包一層贺待,是無法第一次調(diào)用next方法,就輸入?yún)?shù)的零截。

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

for...of循環(huán)可以自動遍歷 Generator 函數(shù)時生成的Iterator對象麸塞,且此時不再需要調(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 (;;) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

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

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

利用for...of循環(huán)两踏,可以寫出遍歷任意對象(object)的方法败京。原生的 JavaScript 對象沒有遍歷接口,無法使用for...of循環(huán)梦染,通過 Generator 函數(shù)為它加上這個接口赡麦,就可以用了。

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 函數(shù)objectEntries為它加上遍歷器接口晶姊,就可以用for...of遍歷了。加上遍歷器接口的另一種寫法是伪货,將 Generator 函數(shù)加到對象的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循環(huán)以外,擴展運算符(...)碱呼、解構賦值和Array.from方法內(nèi)部調(diào)用的蒙挑,都是遍歷器接口。這意味著愚臀,它們都可以將 Generator 函數(shù)返回的 Iterator 對象忆蚀,作為參數(shù)。

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 循環(huán)
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2

Generator.prototype.throw()

Generator 函數(shù)返回的遍歷器對象,都有一個throw方法馋袜,可以在函數(shù)體外拋出錯誤男旗,然后在 Generator 函數(shù)體內(nèi)捕獲。

var g = function* () {
  try {
    yield;
  } catch (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語句捕獲捧毛。i第二次拋出錯誤观堂,由于 Generator 函數(shù)內(nèi)部的catch語句已經(jīng)執(zhí)行過了,不會再捕捉到這個錯誤了呀忧,所以這個錯誤就被拋出了 Generator 函數(shù)體师痕,被函數(shù)體外的catch語句捕獲。

throw方法可以接受一個參數(shù)而账,該參數(shù)會被catch語句接收胰坟,建議拋出Error對象的實例。

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log(e);
  }
};

var i = g();
i.next();
i.throw(new Error('出錯了泞辐!'));
// Error: 出錯了笔横!(…)

注意,不要混淆遍歷器對象的throw方法和全局的throw命令咐吼。上面代碼的錯誤吹缔,是用遍歷器對象的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ù)try代碼塊里面剩余的語句了。

如果 Generator 函數(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

上面代碼中廊移,Generator 函數(shù)g內(nèi)部沒有部署try...catch代碼塊糕簿,所以拋出的錯誤直接被外部catch代碼塊捕獲。

如果 Generator 函數(shù)內(nèi)部和外部画机,都沒有部署try...catch代碼塊冶伞,那么程序?qū)箦e,直接中斷執(zhí)行步氏。

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

var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined

上面代碼中响禽,g.throw拋出錯誤以后,沒有任何try...catch代碼塊可以捕獲這個錯誤,導致程序報錯芋类,中斷執(zhí)行隆嗅。

throw方法拋出的錯誤要被內(nèi)部捕獲,前提是必須至少執(zhí)行過一次next方法侯繁。

function* gen() {
  try {
    yield 1;
  } catch (e) {
    console.log('內(nèi)部捕獲');
  }
}

var g = gen();
g.throw(1);
// Uncaught 1

上面代碼中胖喳,g.throw(1)執(zhí)行時,next方法一次都沒有執(zhí)行過贮竟。這時丽焊,拋出的錯誤不會被內(nèi)部捕獲,而是直接在外部拋出咕别,導致程序出錯技健。這種行為其實很好理解,因為第一次執(zhí)行next方法惰拱,等同于啟動執(zhí)行 Generator 函數(shù)的內(nèi)部代碼雌贱,否則 Generator 函數(shù)還沒有開始執(zhí)行,這時throw方法拋錯只可能拋出在函數(shù)外部偿短。

throw方法被捕獲以后欣孤,會附帶執(zhí)行下一條yield表達式。也就是說昔逗,會附帶執(zhí)行一次next方法降传。

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

var g = gen();
g.next() // a
g.throw() // b
g.next() // c

上面代碼中,g.throw方法被捕獲以后纤子,自動執(zhí)行了一次next方法搬瑰,所以會打印b。另外控硼,也可以看到泽论,只要 Generator 函數(shù)內(nèi)部部署了try...catch代碼塊,那么遍歷器的throw方法拋出的錯誤卡乾,不影響下一次遍歷翼悴。

另外,throw命令與g.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命令拋出的錯誤不會影響到遍歷器的狀態(tài)误堡,所以兩次執(zhí)行next方法古话,都進行了正確的操作。

這種函數(shù)體內(nèi)捕獲錯誤的機制锁施,大大方便了對錯誤的處理陪踩。多個yield表達式杖们,可以只用一個try...catch代碼塊來捕獲錯誤。如果使用回調(diào)函數(shù)的寫法肩狂,想要捕獲多個錯誤摘完,就不得不為每個函數(shù)內(nèi)部寫一個錯誤處理語句,現(xiàn)在只在 Generator 函數(shù)內(nèi)部寫一次catch語句就可以了傻谁。

Generator 函數(shù)體外拋出的錯誤孝治,可以在函數(shù)體內(nèi)捕獲;反過來审磁,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í)行過程中拋出錯誤吕喘,且沒有被內(nèi)部捕獲赘那,就不會再執(zhí)行下去了窥浪。如果此后還調(diào)用next方法仰猖,將返回一個value屬性等于undefined撞反、done屬性等于true的對象纬纪,即 JavaScript 引擎認為這個 Generator 已經(jīng)運行結束了绎秒。

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)結束了,不再執(zhí)行下去了奖年。

Generator.prototype.return()

Generator 函數(shù)返回的遍歷器對象,還有一個return方法沛贪,可以返回給定的值陋守,并且終結遍歷 Generator 函數(shù)揍堰。

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調(diào)用return方法后嗅义,返回值的value屬性就是return方法的參數(shù)foo屏歹。并且,Generator 函數(shù)的遍歷就終止了之碗,返回值的done屬性為true蝙眶,以后再調(diào)用next方法,done屬性總是返回true褪那。

如果return方法調(diào)用時幽纷,不提供參數(shù),則返回值的value屬性為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 函數(shù)內(nèi)部有try...finally代碼塊友浸,那么return方法會推遲到finally代碼塊執(zhí)行完再執(zhí)行。

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

上面代碼中偏窝,調(diào)用return方法后收恢,就開始執(zhí)行finally代碼塊,然后等到finally代碼塊執(zhí)行完祭往,再執(zhí)行return方法。

next()驮肉、throw()、return() 的共同點

next()卵渴、throw()return()這三個方法本質(zhì)上是同一件事瑟啃,可以放在一起理解。它們的作用都是讓 Generator 函數(shù)恢復執(zhí)行错负,并且使用不同的語句替換yield表達式折联。

next()是將yield表達式替換成一個值。

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}

gen.next(1); // Object {value: 1, done: true}
// 相當于將 let result = yield x + y
// 替換成 let result = 1;

上面代碼中清笨,第二個next(1)方法就相當于將yield表達式替換成一個值1。如果next方法沒有參數(shù)检号,就相當于替換成undefined

throw()是將yield表達式替換成一個throw語句最仑。

gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
// 相當于將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯了'));

return()是將yield表達式替換成一個return語句欲芹。

gen.return(2); // Object {value: 2, done: true}
// 相當于將 let result = yield x + y
// 替換成 let result = return 2;

yield* 表達式

如果在 Generator 函數(shù)內(nèi)部,調(diào)用另一個 Generator 函數(shù)浙宜,默認情況下是沒有效果的。

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 函數(shù),在bar里面調(diào)用foo臭觉,是不會有效果的。

這個就需要用到yield*表達式什乙,用來在一個 Generator 函數(shù)里面執(zhí)行另一個 Generator 函數(shù)。

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()) {
    yield 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返回該遍歷器對象的內(nèi)部值。

從語法角度看厦酬,如果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 函數(shù)算凿,有遞歸的效果婚夫。

yield*后面的 Generator 函數(shù)(沒有return語句時),等同于在 Generator 函數(shù)內(nèi)部时捌,部署一個for...of循環(huán)。

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*后面的 Generator 函數(shù)(沒有return語句時),不過是for...of的一種簡寫形式蟀伸,完全可以用后者替代前者。反之迟蜜,在有return語句時,則需要用var value = yield* iterator的形式獲取return語句的值畦戒。

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

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

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

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

實際上甥角,任何數(shù)據(jù)結構只要有 Iterator 接口,就可以被yield*遍歷当犯。

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

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

上面代碼中驰凛,yield表達式返回整個字符串,yield*語句返回單個字符涌献。因為字符串具有 Iterator 接口,所以被yield*遍歷卜壕。

如果被代理的 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()
// {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}

上面代碼在第四次調(diào)用next方法的時候亲桥,屏幕上會有輸出,這是因為函數(shù)fooreturn語句悼凑,向函數(shù)bar提供了返回值。

再看一個例子渔欢。

function* genFuncWithReturn() {
  yield 'a';
  yield 'b';
  return 'The result';
}
function* logReturned(genObj) {
  let result = yield* genObj;
  console.log(result);
}

[...logReturned(genFuncWithReturn())]
// The result
// 值為 [ 'a', 'b' ]

上面代碼中,存在兩次遍歷瘟忱。第一次是擴展運算符遍歷函數(shù)logReturned返回的遍歷器對象奥额,第二次是yield*語句遍歷函數(shù)genFuncWithReturn返回的遍歷器對象。這兩次遍歷的效果是疊加的访诱,最終表現(xiàn)為擴展運算符遍歷函數(shù)genFuncWithReturn返回的遍歷器對象垫挨。所以,最后的數(shù)據(jù)表達式得到的值等于[ 'a', 'b' ]。但是,函數(shù)genFuncWithReturnreturn語句的返回值The result缰冤,會返回給函數(shù)logReturned內(nèi)部的result變量迂苛,因此會有終端輸出。

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*語句遍歷完全二叉樹留储。

// 下面是二叉樹的構造函數(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ù)的this

Generator 函數(shù)總是返回一個遍歷器姓赤,ES6 規(guī)定這個遍歷器是 Generator 函數(shù)的實例,也繼承了 Generator 函數(shù)的prototype對象上的方法涝开。

function* g() {}

g.prototype.hello = function () {
  return 'hi!';
};

let obj = g();

obj instanceof g // true
obj.hello() // 'hi!'

上面代碼表明核偿,Generator 函數(shù)g返回的遍歷器obj妇斤,是g的實例荸恕,而且繼承了g.prototype。但是死相,如果把g當作普通的構造函數(shù)融求,并不會生效,因為g返回的總是遍歷器對象算撮,而不是this對象生宛。

function* g() {
  this.a = 11;
}

let obj = g();
obj.next();
obj.a // undefined

上面代碼中县昂,Generator 函數(shù)gthis對象上面添加了一個屬性a,但是obj對象拿不到這個屬性茅糜。

Generator 函數(shù)也不能跟new命令一起用七芭,會報錯。

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

new F()
// TypeError: F is not a constructor

上面代碼中蔑赘,new命令跟構造函數(shù)F一起使用狸驳,結果報錯,因為F不是構造函數(shù)缩赛。

那么耙箍,有沒有辦法讓 Generator 函數(shù)返回一個正常的對象實例,既可以用next方法酥馍,又可以獲得正常的this辩昆?

下面是一個變通方法。首先旨袒,生成一個空對象汁针,使用call方法綁定 Generator 函數(shù)內(nèi)部的this。這樣砚尽,構造函數(shù)調(diào)用以后施无,這個空對象就是 Generator 函數(shù)的實例對象了。

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {};
var f = F.call(obj);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3

上面代碼中必孤,首先是F內(nèi)部的this對象綁定obj對象猾骡,然后調(diào)用它,返回一個 Iterator 對象敷搪。這個對象執(zhí)行三次next方法(因為F內(nèi)部有兩個yield表達式)兴想,完成 F 內(nèi)部所有代碼的運行。這時赡勘,所有內(nèi)部屬性都綁定在obj對象上了嫂便,因此obj對象也就成了F的實例。

上面代碼中狮含,執(zhí)行的是遍歷器對象f顽悼,但是生成的對象實例是obj,有沒有辦法將這兩個對象統(tǒng)一呢几迄?

一個辦法就是將obj換成F.prototype蔚龙。

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var f = F.call(F.prototype);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

再將F改成構造函數(shù),就可以對它執(zhí)行new命令了映胁。

function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

含義

Generator 與狀態(tài)機

Generator 是實現(xiàn)狀態(tài)機的最佳結構木羹。比如,下面的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)(TickTock)坑填,每運行一次抛人,就改變一次狀態(tài)。這個函數(shù)如果用 Generator 實現(xiàn)脐瑰,就是下面這樣妖枚。

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

上面的 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í)行完畢盯拱,才會結束執(zhí)行父函數(shù)。協(xié)程與其不同例嘱,多個線程(單線程情況下狡逢,即多個函數(shù))可以并行執(zhí)行,但是只有一個線程(或函數(shù))處于正在運行的狀態(tài)拼卵,其他線程(或函數(shù))都處于暫停態(tài)(suspended)奢浑,線程(或函數(shù))之間可以交換執(zhí)行權。也就是說腋腮,一個線程(或函數(shù))執(zhí)行到一半雀彼,可以暫停執(zhí)行,將執(zhí)行權交給另一個線程(或函數(shù))即寡,等到稍后收回執(zhí)行權的時候徊哑,再恢復執(zhí)行。這種可以并行執(zhí)行聪富、交換執(zhí)行權的線程(或函數(shù))莺丑,就稱為協(xié)程。

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

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

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

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

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

如果將 Generator 函數(shù)當作協(xié)程检眯,完全可以將多個需要互相協(xié)作的任務寫成 Generator 函數(shù)厘擂,它們之間使用yield表達式交換控制權。

Generator 與上下文

JavaScript 代碼運行時锰瘸,會產(chǎn)生一個全局的上下文環(huán)境(context刽严,又稱運行環(huán)境),包含了當前所有的變量和對象避凝。然后舞萄,執(zhí)行函數(shù)(或塊級代碼)的時候,又會在當前上下文環(huán)境的上層管削,產(chǎn)生一個函數(shù)運行的上下文倒脓,變成當前(active)的上下文,由此形成一個上下文環(huán)境的堆棧(context stack)含思。

這個堆棧是“后進先出”的數(shù)據(jù)結構崎弃,最后產(chǎn)生的上下文環(huán)境首先執(zhí)行完成,退出堆棧含潘,然后再執(zhí)行完成它下層的上下文饲做,直至所有代碼執(zhí)行完成,堆棧清空遏弱。

Generator 函數(shù)不是這樣盆均,它執(zhí)行產(chǎn)生的上下文環(huán)境,一旦遇到yield命令漱逸,就會暫時退出堆棧泪姨,但是并不消失,里面的所有變量和對象會凍結在當前狀態(tài)饰抒。等到對它執(zhí)行next命令時驴娃,這個上下文環(huán)境又會重新加入調(diào)用棧,凍結的變量和對象恢復執(zhí)行循集。

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

let g = gen();

console.log(
  g.next().value,
  g.next().value,
);

上面代碼中,第一次執(zhí)行g.next()時蔗草,Generator 函數(shù)gen的上下文會加入堆棧咒彤,即開始運行gen內(nèi)部的代碼。等遇到yield 1時咒精,gen上下文退出堆棧镶柱,內(nèi)部狀態(tài)凍結。第二次執(zhí)行g.next()時模叙,gen上下文重新加入堆棧歇拆,變成當前的上下文,重新恢復執(zhí)行。

應用

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

(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界面(showLoadingScreen),并且異步加載數(shù)據(jù)(loadUIDataAsynchronously)腌乡。等到數(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表達式,本身是沒有值的帮辟,總是等于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 改寫上面的代碼润樱。

Promise.resolve(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(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

然后,使用一個函數(shù)店展,按次序自動執(zhí)行所有步驟养篓。

scheduler(longRunningTask(initialValue));

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

注意赂蕴,上面這種做法柳弄,只適合同步操作,即所有的task都必須是同步的概说,不能有異步操作碧注。因為這里的代碼一得到返回值,就繼續(xù)往下執(zhí)行糖赔,沒有判斷異步操作何時完成萍丐。如果要控制異步的操作流程,詳見后面的《異步操作》一章放典。

下面逝变,利用for...of循環(huán)會自動依次執(zhí)行yield命令的特性,提供一種更一般的控制流管理的方法奋构。

let steps = [step1Func, step2Func, step3Func];

function* iterateSteps(steps){
  for (var i=0; i< steps.length; i++){
    var step = steps[i];
    yield step();
  }
}

上面代碼中壳影,數(shù)組steps封裝了一個任務的多個步驟,Generator 函數(shù)iterateSteps則是依次為這些步驟加上yield命令声怔。

將任務分解成步驟之后,還可以將項目分解成多個依次執(zhí)行的任務舱呻。

let jobs = [job1, job2, job3];

function* iterateJobs(jobs){
  for (var i=0; i< jobs.length; i++){
    var job = jobs[i];
    yield* iterateSteps(job.steps);
  }
}

上面代碼中,數(shù)組jobs封裝了一個項目的多個任務份名,Generator 函數(shù)iterateJobs則是依次為這些任務加上yield*命令贩据。

最后,就可以用for...of循環(huán)一次性依次執(zhí)行所有任務的所有步驟柿冲。

for (var step of iterateJobs(jobs)){
  console.log(step.id);
}

再次提醒,上面的做法只能用于所有步驟都是同步操作的情況兆旬,不能有異步操作的步驟假抄。如果想要依次執(zhí)行異步的步驟,必須使用后面的《異步操作》一章介紹的方法丽猬。

for...of的本質(zhì)是一個while循環(huán)宿饱,所以上面的代碼實質(zhì)上執(zhí)行的是下面的邏輯。

var it = iterateJobs(jobs);
var res = it.next();

while (!res.done){
  var result = res.value;
  // ...
  res = it.next();
}

(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ù)結構

Generator 可以看作是數(shù)據(jù)結構炕檩,更確切地說,可以看作是一個數(shù)組結構淮野,因為 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ù)組的接口与学。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末彤悔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子索守,更是在濱河造成了極大的恐慌晕窑,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卵佛,死亡現(xiàn)場離奇詭異杨赤,居然都是意外死亡,警方通過查閱死者的電腦和手機截汪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門疾牲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人挫鸽,你說我怎么就攤上這事说敏。” “怎么了丢郊?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵盔沫,是天一觀的道長。 經(jīng)常有香客問我枫匾,道長架诞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任干茉,我火速辦了婚禮谴忧,結果婚禮上,老公的妹妹穿的比我還像新娘角虫。我一直安慰自己沾谓,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布戳鹅。 她就那樣靜靜地躺著均驶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪枫虏。 梳的紋絲不亂的頭發(fā)上妇穴,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音隶债,去河邊找鬼腾它。 笑死,一個胖子當著我的面吹牛死讹,可吹牛的內(nèi)容都是我干的瞒滴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼赞警,長吁一口氣:“原來是場噩夢啊……” “哼妓忍!你這毒婦竟也來了稀并?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤单默,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后忘瓦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搁廓,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年耕皮,在試婚紗的時候發(fā)現(xiàn)自己被綠了境蜕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡凌停,死狀恐怖粱年,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情罚拟,我是刑警寧澤台诗,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站赐俗,受9級特大地震影響拉队,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜阻逮,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一粱快、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧叔扼,春花似錦事哭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至食呻,卻和暖如春流炕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背仅胞。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工每辟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人干旧。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓渠欺,卻偏偏與公主長得像,于是被迫代替她去往敵國和親椎眯。 傳聞我的和親對象是個殘疾皇子挠将,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 在此處先列下本篇文章的主要內(nèi)容 簡介 next方法的參數(shù) for...of循環(huán) Generator.prototy...
    醉生夢死閱讀 1,436評論 3 8
  • 簡介 基本概念 Generator函數(shù)是ES6提供的一種異步編程解決方案胳岂,語法行為與傳統(tǒng)函數(shù)完全不同。本章詳細介紹...
    呼呼哥閱讀 1,068評論 0 4
  • 不知怎的 一想到要與你為鄰 我的心不再惶惑了 也沒有羞澀了 像一個迷失很久的孩子 要回家了 像一粒懸空漂浮的塵埃 ...
    Cherie雨絲閱讀 278評論 0 0
  • 已經(jīng)開始了,多肉花客看到群里已經(jīng)有人的多肉出了問題内贮。感覺好可怕〔埃現(xiàn)在才剛剛開始,可以說還美還是夜郁。那么有哪些多肉夏天...
    多肉花客閱讀 3,714評論 0 0