ES6學(xué)習(xí)?Generator函數(shù)的語法

簡(jiǎn)介

基本概念

Generator函數(shù)是ES6提供的一種異步編程解決方案,語法行為與傳統(tǒng)函數(shù)完全不同渗钉。
Generator函數(shù)有多種理解角度郭厌。語法上,首先可以把它理解成你踩,Generator函數(shù)是一個(gè)狀態(tài)機(jī)诅岩,封裝了多個(gè)內(nèi)部狀態(tài)。
執(zhí)行Generator函數(shù)會(huì)返回一個(gè)遍歷器對(duì)象带膜,也就是說吩谦,Generator函數(shù)除了狀態(tài)機(jī),還是一個(gè)遍歷器對(duì)象生成函數(shù)膝藕。返回的遍歷器對(duì)象式廷,可以依次遍歷Generator函數(shù)內(nèi)部的每一個(gè)狀態(tài)。
形式上芭挽,Generator函數(shù)是一個(gè)普通函數(shù)滑废,但是有兩個(gè)特征。一是袜爪,function關(guān)鍵字與函數(shù)名之間有一個(gè)星號(hào)蠕趁;二是,函數(shù)體內(nèi)部使用yield表達(dá)式辛馆,定義不同的內(nèi)部狀態(tài)(yield在英語里的意思就是“產(chǎn)出”)俺陋。

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

上面代碼定義了一個(gè)Generator函數(shù)helloWorldGenerator,它內(nèi)部有兩個(gè)yield表達(dá)式昙篙,即該函數(shù)有三個(gè)狀態(tài):hello腊状,worldreturn語句。
然后瓢对,Generator函數(shù)的調(diào)用方法與普通函數(shù)一樣寿酌。不同的是,調(diào)用Generator函數(shù)后硕蛹,該函數(shù)并不執(zhí)行醇疼,返回的也不是函數(shù)運(yùn)行結(jié)果硕并,而是一個(gè)指向內(nèi)部狀態(tài)的指針對(duì)象,也就是遍歷器對(duì)象(Iterator Object)秧荆。
下一步倔毙,必須調(diào)用遍歷器對(duì)象的next方法,使得指針移向下一個(gè)狀態(tài)乙濒。也就是說陕赃,每次調(diào)用next方法,內(nèi)部指針就從函數(shù)頭部或上一次停下來的地方開始執(zhí)行颁股,直到遇到下一個(gè)yield表達(dá)式(或return語句)為止。換言之诉儒,Generator函數(shù)是分段執(zhí)行的忱反,yield表達(dá)式是暫停執(zhí)行的標(biāo)記滤愕,而next方法可以恢復(fù)執(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í)行注竿,直到遇到第一個(gè)yield表達(dá)式為止。next方法返回一個(gè)對(duì)象宇智,它的value屬性就是當(dāng)前yield表達(dá)式的值hello蔓搞,done屬性的值false胰丁,表示遍歷還沒有結(jié)束随橘。
第二次調(diào)用,Generator函數(shù)從上次yield表達(dá)式停下的地方锦庸,一直執(zhí)行到下一個(gè)yield表達(dá)式机蔗。next方法返回的對(duì)象的value屬性就是當(dāng)前yield表達(dá)式的值worlddone屬性的值false甘萧,表示遍歷還沒有結(jié)束萝嘁。
第三次調(diào)用,Generator函數(shù)從上次yield表達(dá)式停下的地方牙言,一直執(zhí)行到return語句(如果沒有return語句卑硫,就執(zhí)行到函數(shù)結(jié)束)。next方法返回的對(duì)象的value屬性,就是緊跟在return語句后面的表達(dá)式的值(如果沒有return語句障陶,則value屬性的值為undefined),done屬性的值true,表示遍歷已經(jīng)結(jié)束侄刽。
第四次調(diào)用杂彭,此時(shí)Generator函數(shù)已經(jīng)運(yùn)行完畢,next方法返回對(duì)象的value屬性為undefined主胧,done屬性為true。以后再調(diào)用next方法,返回的都是這個(gè)值囤官。
總結(jié)一下,調(diào)用Generator函數(shù)见坑,返回一個(gè)遍歷器對(duì)象,代表Generator函數(shù)的內(nèi)部指針。以后鲫骗,每次調(diào)用遍歷器對(duì)象的next方法,就會(huì)返回一個(gè)有著valuedone兩個(gè)屬性的對(duì)象。value屬性表示當(dāng)前的內(nèi)部狀態(tài)的值排苍,是yield表達(dá)式后面那個(gè)表達(dá)式的值;done屬性是一個(gè)布爾值漩氨,表示是否遍歷結(jié)束款青。
ES6沒有規(guī)定饰及,function關(guān)鍵字與函數(shù)名之間的星號(hào)腿短,寫在哪個(gè)位置赴魁。這導(dǎo)致下面的寫法都能通過。

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

由于Generator函數(shù)仍然是普通函數(shù),所以一般的寫法是上面的第三種,即星號(hào)緊跟在function關(guān)鍵字后面盔腔。

yield表達(dá)式

由于Generator函數(shù)返回的遍歷器對(duì)象宁赤,只有調(diào)用next方法才會(huì)遍歷下一個(gè)內(nèi)部狀態(tài)愕够,所以其實(shí)提供了一種可以暫停執(zhí)行的函數(shù)。yield表達(dá)式就是暫停標(biāo)志遂跟。
遍歷器對(duì)象的next方法的運(yùn)行邏輯如下。

  1. (1)遇到yield表達(dá)式,就暫停執(zhí)行后面的操作,并將緊跟在yield后面的那個(gè)表達(dá)式的值媒峡,作為返回的對(duì)象的value屬性值酬滤。
  • (2)下一次調(diào)用next方法時(shí),再繼續(xù)往下執(zhí)行冠摄,直到遇到下一個(gè)yield表達(dá)式拆挥。
    (3)如果沒有再遇到新的yield表達(dá)式否副,就一直運(yùn)行到函數(shù)結(jié)束负甸,直到return語句為止,并將return語句后面的表達(dá)式的值,作為返回的對(duì)象的value屬性值迫淹。
    (4)如果該函數(shù)沒有return語句第股,則返回的對(duì)象的value屬性值為undefined
    需要注意的是,yield表達(dá)式后面的表達(dá)式,只有當(dāng)調(diào)用next方法、內(nèi)部指針指向該語句時(shí)才會(huì)執(zhí)行冀瓦,因此等于為JavaScript提供了手動(dòng)的“惰性求值”(Lazy Evaluation)的語法功能宣鄙。
function* gen() {
  yield  123 + 456;
}

上面代碼中,yield后面的表達(dá)式123 + 456聚谁,不會(huì)立即求值,只會(huì)在next方法將指針移到這一句時(shí)滞诺,才會(huì)求值处嫌。
yield表達(dá)式與return語句既有相似之處赚楚,也有區(qū)別。相似之處在于捕虽,都能返回緊跟在語句后面的那個(gè)表達(dá)式的值泻骤。區(qū)別在于每次遇到yield诊沪,函數(shù)暫停執(zhí)行诀诊,下一次再從該位置繼續(xù)向后執(zhí)行,而return語句不具備位置記憶的功能蒜田。一個(gè)函數(shù)里面襟铭,只能執(zhí)行一次(或者說一個(gè))return語句,但是可以執(zhí)行多次(或者說多個(gè))yield表達(dá)式贱傀。正常函數(shù)只能返回一個(gè)值,因?yàn)橹荒軋?zhí)行一次return糊探;Generator函數(shù)可以返回一系列的值,因?yàn)榭梢杂腥我舛鄠€(gè)yield查蓉。從另一個(gè)角度看缸匪,也可以說Generator生成了一系列的值译暂,這也就是它的名稱的來歷(英語中,generator這個(gè)詞是“生成器”的意思)撩炊。
Generator函數(shù)可以不用yield表達(dá)式外永,這時(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表達(dá)式只能用在Generator函數(shù)里面愉择,用在其他地方都會(huì)報(bào)錯(cuò)劫乱。

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

上面代碼在一個(gè)普通函數(shù)中使用yield表達(dá)式,結(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)?code>forEach方法的參數(shù)是一個(gè)普通函數(shù)层坠,但是在里面使用了yield表達(dá)式(這個(gè)函數(shù)里面還使用了yield*表達(dá)式)殖妇。一種修改方法是改用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表達(dá)式如果用在另一個(gè)表達(dá)式之中谦趣,必須放在圓括號(hào)里面疲吸。

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表達(dá)式用作函數(shù)參數(shù)或放在賦值表達(dá)式的右邊,可以不加括號(hào)前鹅。

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

與Iterator接口的關(guān)系

上一章說過摘悴,任意一個(gè)對(duì)象的Symbol.iterator方法,等于該對(duì)象的遍歷器生成函數(shù)舰绘,調(diào)用該函數(shù)會(huì)返回該對(duì)象的一個(gè)遍歷器對(duì)象蹂喻。
由于Generator函數(shù)就是遍歷器生成函數(shù),因此可以把Generator賦值給對(duì)象的Symbol.iterator屬性捂寿,從而使得該對(duì)象具有Iterator接口口四。

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

上面代碼中,Generator函數(shù)賦值給Symbol.iterator屬性秦陋,從而使得myIterable對(duì)象具有了Iterator接口蔓彩,可以被...運(yùn)算符遍歷了。
Generator函數(shù)執(zhí)行后驳概,返回一個(gè)遍歷器對(duì)象赤嚼。該對(duì)象本身也具有Symbol.iterator屬性,執(zhí)行后返回自身抡句。

function* gen(){
  // some code
}
var g = gen();
g[Symbol.iterator]() === g // true

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

next方法的參數(shù)

yield表達(dá)式本身沒有返回值,或者說總是返回undefined绳瘟。next方法可以帶一個(gè)參數(shù)雕憔,該參數(shù)就會(huì)被當(dāng)作上一個(gè)yield表達(dá)式的返回值。

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è)可以無限運(yùn)行的Generator函數(shù)f糖声,如果next方法沒有參數(shù)斤彼,每次運(yùn)行到yield表達(dá)式,變量reset的值總是undefined蘸泻。當(dāng)next方法帶一個(gè)參數(shù)true時(shí)琉苇,變量reset就被重置為這個(gè)參數(shù)(即true),因此i會(huì)等于-1悦施,下一輪循環(huán)就會(huì)從-1開始遞增并扇。
這個(gè)功能有很重要的語法意義。Generator函數(shù)從暫停狀態(tài)到恢復(fù)運(yùn)行抡诞,它的上下文狀態(tài)(context)是不變的穷蛹。通過next方法的參數(shù)土陪,就有辦法在Generator函數(shù)開始運(yùn)行之后,繼續(xù)向函數(shù)體內(nèi)部注入值肴熏。也就是說鬼雀,可以在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: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 }

上面代碼中出刷,第二次運(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é)果就完全不一樣了系瓢。上面代碼第一次調(diào)用bnext方法時(shí),返回x+1的值6句灌;第二次調(diào)用next方法夷陋,將上一次yield表達(dá)式的值設(shè)為12,因此y等于24胰锌,返回y / 3的值8骗绕;第三次調(diào)用next方法,將上一次yield表達(dá)式的值設(shè)為13资昧,因此z等于13酬土,這時(shí)x等于5,y等于24格带,所以return語句的值等于42撤缴。
注意,由于next方法的參數(shù)表示上一個(gè)yield表達(dá)式的返回值叽唱,所以在第一次使用next方法時(shí)屈呕,傳遞參數(shù)是無效的。V8引擎直接忽略第一次使用next方法時(shí)的參數(shù)尔觉,只有從第二次使用next方法開始凉袱,參數(shù)才是有效的。從語義上講,第一個(gè)next方法用來啟動(dòng)遍歷器對(duì)象专甩,所以不用帶有參數(shù)钟鸵。
再看一個(gè)通過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

上面代碼是一個(gè)很直觀的例子棺耍,每次通過next方法向Generator函數(shù)輸入值,然后打印出來种樱。
如果想要第一次調(diào)用next方法時(shí)蒙袍,就能夠輸入值,可以在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)可以自動(dòng)遍歷Generator函數(shù)時(shí)生成的Iterator對(duì)象,且此時(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表達(dá)式的值。這里需要注意恰矩,一旦next方法的返回對(duì)象的done屬性為true记盒,for...of循環(huán)就會(huì)中止,且不包含該返回對(duì)象外傅,所以上面代碼的return語句返回的6纪吮,不包括在for...of循環(huán)之中。
下面是一個(gè)利用Generator函數(shù)和for...of循環(huán)栏豺,實(shí)現(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語句時(shí)不需要使用next方法奥洼。
利用for...of循環(huán),可以寫出遍歷任意對(duì)象的方法晚胡。原生的JavaScript對(duì)象沒有遍歷接口灵奖,無法使用for...of循環(huán),通過Generator函數(shù)為它加上這個(gè)接口估盘,就可以用了瓷患。

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

上面代碼中,對(duì)象jane原生不具備Iterator接口遣妥,無法用for...of遍歷擅编。這時(shí),我們通過Generator函數(shù)objectEntries為它加上遍歷器接口,就可以用for...of遍歷了爱态。加上遍歷器接口的另一種寫法是谭贪,將Generator函數(shù)加到對(duì)象的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)以外锦担,擴(kuò)展運(yùn)算符(...)俭识、解構(gòu)賦值和Array.from方法內(nèi)部調(diào)用的,都是遍歷器接口洞渔。這意味著套媚,它們都可以將Generator函數(shù)返回的Iterator對(duì)象,作為參數(shù)磁椒。

function* numbers () {
  yield 1
  yield 2
  return 3
  yield 4
}
// 擴(kuò)展運(yùn)算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解構(gòu)賦值
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ù)返回的遍歷器對(duì)象堤瘤,都有一個(gè)throw方法,可以在函數(shù)體外拋出錯(cuò)誤浆熔,然后在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

上面代碼中,遍歷器對(duì)象i連續(xù)拋出兩個(gè)錯(cuò)誤蘸拔。第一個(gè)錯(cuò)誤被Generator函數(shù)體內(nèi)的catch語句捕獲师郑。i第二次拋出錯(cuò)誤,由于Generator函數(shù)內(nèi)部的catch語句已經(jīng)執(zhí)行過了调窍,不會(huì)再捕捉到這個(gè)錯(cuò)誤了宝冕,所以這個(gè)錯(cuò)誤就被拋出了Generator函數(shù)體,被函數(shù)體外的catch語句捕獲邓萨。
throw方法可以接受一個(gè)參數(shù)地梨,該參數(shù)會(huì)被catch語句接收,建議拋出Error對(duì)象的實(shí)例缔恳。

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log(e);
  }
};
var i = g();
i.next();
i.throw(new Error('出錯(cuò)了宝剖!'));
// Error: 出錯(cuò)了!(…)

注意歉甚,不要混淆遍歷器對(duì)象的throw方法和全局的throw命令万细。上面代碼的錯(cuò)誤,是用遍歷器對(duì)象的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聘裁,是因?yàn)楹瘮?shù)體外的catch語句塊雪营,捕獲了拋出的a錯(cuò)誤以后,就不會(huì)再繼續(xù)try代碼塊里面剩余的語句了衡便。
如果Generator函數(shù)內(nè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

上面代碼中谴餐,Generator函數(shù)g內(nèi)部沒有部署try...catch代碼塊姻政,所以拋出的錯(cuò)誤直接被外部catch代碼塊捕獲。
如果Generator函數(shù)內(nèi)部和外部总寒,都沒有部署try...catch代碼塊扶歪,那么程序?qū)?bào)錯(cuò),直接中斷執(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拋出錯(cuò)誤以后,沒有任何try...catch代碼塊可以捕獲這個(gè)錯(cuò)誤年枕,導(dǎo)致程序報(bào)錯(cuò)炫欺,中斷執(zhí)行。
throw方法拋出的錯(cuò)誤要被內(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í)行時(shí)摩桶,next方法一次都沒有執(zhí)行過桥状。這時(shí),拋出的錯(cuò)誤不會(huì)被內(nèi)部捕獲硝清,而是直接在外部拋出辅斟,導(dǎo)致程序出錯(cuò)。這種行為其實(shí)很好理解芦拿,因?yàn)榈谝淮螆?zhí)行next方法士飒,等同于啟動(dòng)執(zhí)行Generator函數(shù)的內(nèi)部代碼,否則Generator函數(shù)還沒有開始執(zhí)行蔗崎,這時(shí)throw方法拋錯(cuò)只可能拋出在函數(shù)外部酵幕。
throw方法被捕獲以后,會(huì)附帶執(zhí)行下一條yield表達(dá)式缓苛。也就是說芳撒,會(huì)附帶執(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方法被捕獲以后番官,自動(dòng)執(zhí)行了一次next方法,所以會(huì)打印b钢属。另外,也可以看到门躯,只要Generator函數(shù)內(nèi)部部署了try...catch代碼塊淆党,那么遍歷器的throw方法拋出的錯(cuò)誤,不影響下一次遍歷。
另外染乌,throw命令與g.throw方法是無關(guān)的山孔,兩者互不影響。

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方法,都進(jìn)行了正確的操作勒庄。
這種函數(shù)體內(nèi)捕獲錯(cuò)誤的機(jī)制串前,大大方便了對(duì)錯(cuò)誤的處理。多個(gè)yield表達(dá)式实蔽,可以只用一個(gè)try...catch代碼塊來捕獲錯(cuò)誤荡碾。如果使用回調(diào)函數(shù)的寫法,想要捕獲多個(gè)錯(cuò)誤局装,就不得不為每個(gè)函數(shù)內(nèi)部寫一個(gè)錯(cuò)誤處理語句坛吁,現(xiàn)在只在Generator函數(shù)內(nèi)部寫一次catch語句就可以了。
Generator函數(shù)體外拋出的錯(cuò)誤铐尚,可以在函數(shù)體內(nè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ù)值是沒有toUpperCase方法的匆骗,所以會(huì)拋出一個(gè)TypeError錯(cuò)誤,被函數(shù)體外的catch捕獲誉简。
一旦Generator執(zhí)行過程中拋出錯(cuò)誤碉就,且沒有被內(nèi)部捕獲,就不會(huì)再執(zhí)行下去了闷串。如果此后還調(diào)用next方法瓮钥,將返回一個(gè)value屬性等于undefineddone屬性等于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í)行下去了凉蜂。

Generator.prototype.return()

Generator函數(shù)返回的遍歷器對(duì)象琼梆,還有一個(gè)return方法性誉,可以返回給定的值,并且終結(jié)遍歷 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 }

上面代碼中错览,遍歷器對(duì)象g調(diào)用return方法后,返回值的value屬性就是return方法的參數(shù)foo煌往。并且倾哺,Generator函數(shù)的遍歷就終止了,返回值的done屬性為true刽脖,以后再調(diào)用next方法羞海,done屬性總是返回true
如果return方法調(diào)用時(shí)曾棕,不提供參數(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方法會(huì)推遲到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() 的共同點(diǎn)

next()厅瞎、throw()饰潜、return()這三個(gè)方法本質(zhì)上是同一件事,可以放在一起理解和簸。它們的作用都是讓Generator函數(shù)恢復(fù)執(zhí)行彭雾,并且使用不同的語句替換yield表達(dá)式。
next()是將yield表達(dá)式替換成一個(gè)值锁保。

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}
// 相當(dāng)于將 let result = yield x + y
// 替換成 let result = 1;

上面代碼中薯酝,第二個(gè)next(1)方法就相當(dāng)于將yield表達(dá)式替換成一個(gè)值1。如果next方法沒有參數(shù)爽柒,就相當(dāng)于替換成undefined吴菠。
throw()是將yield表達(dá)式替換成一個(gè)throw語句。

gen.throw(new Error('出錯(cuò)了')); // Uncaught Error: 出錯(cuò)了
// 相當(dāng)于將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯(cuò)了'));
return()是將yield表達(dá)式替換成一個(gè)return語句浩村。

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

yield*表達(dá)式

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

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,是不會(huì)有效果的棠涮。
這個(gè)就需要用到yield*表達(dá)式,用來在一個(gè)Generator函數(shù)里面執(zhí)行另一個(gè)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"

再來看一個(gè)對(duì)比的例子严肪。

function* inner() {
  yield 'hello!';
}
function* outer1() {
  yield 'open';
  yield inner();
  yield 'close';
}
var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一個(gè)遍歷器對(duì)象
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沒使用驳糯。結(jié)果就是,outer1返回一個(gè)遍歷器對(duì)象氢橙,outer2返回該遍歷器對(duì)象的內(nèi)部值酝枢。
從語法角度看,如果yield表達(dá)式后面跟的是一個(gè)遍歷器對(duì)象悍手,需要在yield表達(dá)式后面加上星號(hào)帘睦,表明它返回的是一個(gè)遍歷器對(duì)象。這被稱為yield*表達(dá)式坦康。

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語句得到的值古胆,是一個(gè)遍歷器,所以要用星號(hào)表示筛璧。運(yùn)行結(jié)果就是使用一個(gè)遍歷器逸绎,遍歷了多個(gè)Generator函數(shù),有遞歸的效果夭谤。
yield*后面的Generator函數(shù)(沒有return語句時(shí))棺牧,等同于在Generator函數(shù)內(nèi)部,部署一個(gè)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語句時(shí)),不過是for...of的一種簡(jiǎn)寫形式采蚀,完全可以用后者替代前者疲牵。反之,在有return語句時(shí)榆鼠,則需要用var value = yield* iterator的形式獲取return語句的值纲爸。
如果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ù)組的遍歷器對(duì)象家妆。
實(shí)際上,任何數(shù)據(jù)結(jié)構(gòu)只要有Iterator接口冕茅,就可以被yield*遍歷伤极。

let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"

上面代碼中,yield表達(dá)式返回整個(gè)字符串姨伤,yield*語句返回單個(gè)字符哨坪。因?yàn)樽址哂?code>Iterator接口,所以被yield*遍歷乍楚。
如果被代理的Generator函數(shù)有return語句当编,那么就可以向代理它的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() // {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í)候徒溪,屏幕上會(huì)有輸出忿偷,這是因?yàn)楹瘮?shù)fooreturn語句,向函數(shù)bar提供了返回值词渤。
再看一個(gè)例子牵舱。

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' ]

上面代碼中,存在兩次遍歷缺虐。第一次是擴(kuò)展運(yùn)算符遍歷函數(shù)logReturned返回的遍歷器對(duì)象芜壁,第二次是yield*語句遍歷函數(shù)genFuncWithReturn返回的遍歷器對(duì)象。這兩次遍歷的效果是疊加的高氮,最終表現(xiàn)為擴(kuò)展運(yùn)算符遍歷函數(shù)genFuncWithReturn返回的遍歷器對(duì)象慧妄。所以,最后的數(shù)據(jù)表達(dá)式得到的值等于[ 'a', 'b' ]剪芍。但是塞淹,函數(shù)genFuncWithReturnreturn語句的返回值The result,會(huì)返回給函數(shù)logReturned內(nèi)部的result變量罪裹,因此會(huì)有終端輸出饱普。
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*語句遍歷完全二叉樹套耕。

// 下面是二叉樹的構(gòu)造函數(shù),
// 三個(gè)參數(shù)分別是左樹峡继、當(dāng)前節(jié)點(diǎn)和右樹
function Tree(left, label, right) {
  this.left = left;
  this.label = label;
  this.right = right;
}
// 下面是中序(inorder)遍歷函數(shù)冯袍。
// 由于返回的是一個(gè)遍歷器,所以要用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é)點(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']]]);
// 遍歷二叉樹
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)寫成下面的形式业汰。

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

上面代碼中鸠姨,myGeneratorMethod屬性前面有一個(gè)星號(hào)坯屿,表示這個(gè)屬性是一個(gè)Generator函數(shù)。
它的完整形式如下资盅,與上面的寫法是等價(jià)的调榄。

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

Generator函數(shù)的this

Generator函數(shù)總是返回一個(gè)遍歷器,ES6規(guī)定這個(gè)遍歷器是Generator函數(shù)的實(shí)例呵扛,也繼承了Generator函數(shù)的prototype對(duì)象上的方法。

function* g() {}
g.prototype.hello = function () {
  return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'

上面代碼表明筐带,Generator函數(shù)g返回的遍歷器obj今穿,是g的實(shí)例,而且繼承了g.prototype伦籍。但是蓝晒,如果把g當(dāng)作普通的構(gòu)造函數(shù),并不會(huì)生效帖鸦,因?yàn)?code>g返回的總是遍歷器對(duì)象芝薇,而不是this對(duì)象。

function* g() {
  this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined

上面代碼中作儿,Generator函數(shù)gthis對(duì)象上面添加了一個(gè)屬性a洛二,但是obj對(duì)象拿不到這個(gè)屬性。
Generator函數(shù)也不能跟new命令一起用攻锰,會(huì)報(bào)錯(cuò)晾嘶。

function* F() {
  yield this.x = 2;
  yield this.y = 3;
}
new F() // TypeError: F is not a constructor

上面代碼中,new命令跟構(gòu)造函數(shù)F一起使用娶吞,結(jié)果報(bào)錯(cuò)垒迂,因?yàn)?code>F不是構(gòu)造函數(shù)。
那么妒蛇,有沒有辦法讓Generator函數(shù)返回一個(gè)正常的對(duì)象實(shí)例机断,既可以用next方法,又可以獲得正常的this绣夺?
下面是一個(gè)變通方法吏奸。首先,生成一個(gè)空對(duì)象乐导,使用call方法綁定Generator函數(shù)內(nèi)部的this苦丁。這樣,構(gòu)造函數(shù)調(diào)用以后物臂,這個(gè)空對(duì)象就是Generator函數(shù)的實(shí)例對(duì)象了旺拉。

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對(duì)象綁定obj對(duì)象,然后調(diào)用它蛾狗,返回一個(gè)Iterator對(duì)象晋涣。這個(gè)對(duì)象執(zhí)行三次next方法(因?yàn)?code>F內(nèi)部有兩個(gè)yield表達(dá)式),完成F內(nèi)部所有代碼的運(yùn)行沉桌。這時(shí)谢鹊,所有內(nèi)部屬性都綁定在obj對(duì)象上了,因此obj對(duì)象也就成了F的實(shí)例留凭。
上面代碼中佃扼,執(zhí)行的是遍歷器對(duì)象f,但是生成的對(duì)象實(shí)例是obj蔼夜,有沒有辦法將這兩個(gè)對(duì)象統(tǒng)一呢兼耀?
一個(gè)辦法就是將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改成構(gòu)造函數(shù)求冷,就可以對(duì)它執(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)機(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)(TickTock),每運(yùn)行一次韭山,就改變一次狀態(tài)郁季。這個(gè)函數(shù)如果用Generator實(shí)現(xiàn),就是下面這樣掠哥。

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

上面的Generator實(shí)現(xiàn)與ES5實(shí)現(xiàn)對(duì)比巩踏,可以看到少了用來保存狀態(tài)的外部變量ticking,這樣就更簡(jiǎn)潔续搀,更安全(狀態(tài)不會(huì)被非法篡改)塞琼、更符合函數(shù)式編程的思想,在寫法上也更優(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)。也就是說罢吃,一個(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),也就是說割去,協(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é)程自己分配。
由于JavaScript是單線程語言邦蜜,只能保持一個(gè)調(diào)用棧依鸥。引入?yún)f(xié)程以后,每個(gè)任務(wù)可以保持自己的調(diào)用棧悼沈。這樣做的最大好處贱迟,就是拋出錯(cuò)誤的時(shí)候,可以找到原始的調(diào)用棧絮供。不至于像異步操作的回調(diào)函數(shù)那樣衣吠,一旦出錯(cuò),原始的調(diào)用棧早就結(jié)束壤靶。
Generator函數(shù)是ES6對(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ù)寫成Generator函數(shù)浓恳,它們之間使用yield表達(dá)式交換控制權(quán)刹缝。

Generator與上下文

JavaScript代碼運(yùn)行時(shí),會(huì)產(chǎn)生一個(gè)全局的上下文環(huán)境(context颈将,又稱運(yùn)行環(huán)境)梢夯,包含了當(dāng)前所有的變量和對(duì)象。然后晴圾,執(zhí)行函數(shù)(或塊級(jí)代碼)的時(shí)候颂砸,又會(huì)在當(dāng)前上下文環(huán)境的上層,產(chǎn)生一個(gè)函數(shù)運(yùn)行的上下文疑务,變成當(dāng)前(active)的上下文沾凄,由此形成一個(gè)上下文環(huán)境的堆棧(context stack)。
這個(gè)堆棧是“后進(jìn)先出”的數(shù)據(jù)結(jié)構(gòu)知允,最后產(chǎn)生的上下文環(huán)境首先執(zhí)行完成撒蟀,退出堆棧,然后再執(zhí)行完成它下層的上下文温鸽,直至所有代碼執(zhí)行完成保屯,堆棧清空手负。
Generator函數(shù)不是這樣,它執(zhí)行產(chǎn)生的上下文環(huán)境姑尺,一旦遇到yield命令竟终,就會(huì)暫時(shí)退出堆棧,但是并不消失切蟋,里面的所有變量和對(duì)象會(huì)凍結(jié)在當(dāng)前狀態(tài)统捶。等到對(duì)它執(zhí)行next命令時(shí),這個(gè)上下文環(huán)境又會(huì)重新加入調(diào)用棧柄粹,凍結(jié)的變量和對(duì)象恢復(fù)執(zhí)行喘鸟。

function* gen() {
  yield 1;
  return 2;
}
let g = gen();
console.log(
  g.next().value,
  g.next().value,
);

上面代碼中,第一次執(zhí)行g.next()時(shí)驻右,Generator函數(shù)gen的上下文會(huì)加入堆棧什黑,即開始運(yùn)行gen內(nèi)部的代碼。等遇到yield 1時(shí)堪夭,gen上下文退出堆棧愕把,內(nèi)部狀態(tài)凍結(jié)。第二次執(zhí)行g.next()時(shí)森爽,gen上下文重新加入堆棧恨豁,變成當(dāng)前的上下文,重新恢復(fù)執(zhí)行爬迟。

應(yīng)用

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

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

Generator函數(shù)的暫停執(zhí)行的效果,意味著可以把異步操作寫在yield表達(dá)式里面捧请,等到調(diào)用next方法時(shí)再往后執(zhí)行凡涩。這實(shí)際上等同于不需要寫回調(diào)函數(shù)了,因?yàn)楫惒讲僮鞯暮罄m(xù)操作可以放在yield表達(dá)式下面疹蛉,反正要等到調(diào)用next方法時(shí)再執(zhí)行活箕。所以,Generator函數(shù)的一個(gè)重要實(shí)際意義就是用來處理異步操作可款,改寫回調(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界面(showLoadingScreen)悉罕,并且異步加載數(shù)據(jù)(loadUIDataAsynchronously)赤屋。等到數(shù)據(jù)加載完成,再一次使用next方法壁袄,則會(huì)隱藏Loading界面类早。可以看到嗜逻,這種寫法的好處是所有Loading界面的邏輯涩僻,都被封裝在一個(gè)函數(shù),按部就班非常清晰栈顷。
Ajax是典型的異步操作逆日,通過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ù)屏富,就是通過Ajax操作獲取數(shù)據(jù)⊥苈保可以看到狠半,除了多了一個(gè)yield,它幾乎與同步操作的寫法完全一樣颤难。注意神年,makeAjaxCall函數(shù)中的next方法,必須加上response參數(shù)行嗤,因?yàn)?code>yield表達(dá)式已日,本身是沒有值的,總是等于undefined栅屏。
下面是另一個(gè)例子飘千,通過Generator函數(shù)逐行讀取文本文件。

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

上面代碼打開文本文件栈雳,使用yield表達(dá)式可以手動(dòng)逐行讀取文件护奈。

2.控制流管理

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

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ù)可以進(jìn)一步改善代碼運(yùn)行流程擅憔。

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

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

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

注意棉姐,上面這種做法,只適合同步操作啦逆,即所有的task都必須是同步的伞矩,不能有異步操作。因?yàn)檫@里的代碼一得到返回值夏志,就繼續(xù)往下執(zhí)行乃坤,沒有判斷異步操作何時(shí)完成。
下面沟蔑,利用for...of循環(huán)會(huì)自動(dòng)依次執(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封裝了一個(gè)任務(wù)的多個(gè)步驟厅须,Generator函數(shù)iterateSteps則是依次為這些步驟加上yield命令。
將任務(wù)分解成步驟之后食棕,還可以將項(xiàng)目分解成多個(gè)依次執(zhí)行的任務(wù)朗和。

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封裝了一個(gè)項(xiàng)目的多個(gè)任務(wù)簿晓,Generator函數(shù)iterateJobs則是依次為這些任務(wù)加上yield*命令眶拉。
最后,就可以用for...of循環(huán)一次性依次執(zhí)行所有任務(wù)的所有步驟憔儿。

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

再次提醒忆植,上面的做法只能用于所有步驟都是同步操作的情況,不能有異步操作的步驟谒臼。如果想要依次執(zhí)行異步的步驟朝刊,必須使用后面的《異步操作》一章介紹的方法。
for...of的本質(zhì)是一個(gè)while循環(huán)蜈缤,所以上面的代碼實(shí)質(zhì)上執(zhí)行的是下面的邏輯坞古。

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

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ì)象织堂,通過iterEntries函數(shù)叠艳,就有了Iterator接口。也就是說易阳,可以在任意對(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)拒课,更確切地說徐勃,可以看作是一個(gè)數(shù)組結(jié)構(gòu),因?yàn)?code>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ù)組的接口暴氏。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末延塑,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子答渔,更是在濱河造成了極大的恐慌关带,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沼撕,死亡現(xiàn)場(chǎng)離奇詭異宋雏,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)务豺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門磨总,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人笼沥,你說我怎么就攤上這事蚪燕。” “怎么了奔浅?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵馆纳,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我汹桦,道長(zhǎng)鲁驶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任舞骆,我火速辦了婚禮钥弯,結(jié)果婚禮上径荔,老公的妹妹穿的比我還像新娘。我一直安慰自己脆霎,他們只是感情好总处,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绪穆,像睡著了一般辨泳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上玖院,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天菠红,我揣著相機(jī)與錄音,去河邊找鬼难菌。 笑死试溯,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的郊酒。 我是一名探鬼主播遇绞,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼燎窘!你這毒婦竟也來了摹闽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤褐健,失蹤者是張志新(化名)和其女友劉穎付鹿,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚜迅,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡舵匾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谁不。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坐梯。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖刹帕,靈堂內(nèi)的尸體忽然破棺而出吵血,到底是詐尸還是另有隱情,我是刑警寧澤偷溺,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布蹋辅,位于F島的核電站,受9級(jí)特大地震影響亡蓉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜喷舀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一砍濒、第九天 我趴在偏房一處隱蔽的房頂上張望淋肾。 院中可真熱鬧,春花似錦爸邢、人聲如沸樊卓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碌尔。三九已至,卻和暖如春券敌,著一層夾襖步出監(jiān)牢的瞬間唾戚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工待诅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留叹坦,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓卑雁,卻偏偏與公主長(zhǎng)得像募书,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子测蹲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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

  • 在此處先列下本篇文章的主要內(nèi)容 簡(jiǎn)介 next方法的參數(shù) for...of循環(huán) Generator.prototy...
    醉生夢(mèng)死閱讀 1,440評(píng)論 3 8
  • 簡(jiǎn)介 基本概念 Generator函數(shù)是ES6提供的一種異步編程解決方案莹捡,語法行為與傳統(tǒng)函數(shù)完全不同。本章詳細(xì)介紹...
    呼呼哥閱讀 1,075評(píng)論 0 4
  • Generator 函數(shù)的語法 簡(jiǎn)介 基本概念 Generator 函數(shù)是 ES6 提供的一種異步編程解決方案扣甲,語...
    站在大神的肩膀上看世界閱讀 4,170評(píng)論 0 6
  • 同步應(yīng)用 簡(jiǎn)介 基本概念 Generator函數(shù)式ES6提供的一種異步編程解決方案,語法行為和傳統(tǒng)函數(shù)完全不同.語...
    JarvanZ閱讀 1,191評(píng)論 0 1
  • 夜擁深更夜怨重篮赢,堆雪海棠夢(mèng),落梧桐文捶。曉風(fēng)蕭蕭說窮情荷逞,想不盡,心關(guān)鎖千重粹排。 冷月勾倦簾种远,無心理情懷,來晨風(fēng)顽耳。寒濃揉破...
    易安閱讀 306評(píng)論 0 1