生成器

生成器簡介

基本概念

生成器函數(shù)是ES6提供的一種異步編程解決方案必指,語法行為與傳統(tǒng)函數(shù)完全不同医咨。它可以被看成一個(gè)封裝了很多內(nèi)部狀態(tài)的狀態(tài)機(jī)母剥,除此之外他還是一個(gè)迭代器對象的生成函數(shù)误续,生成的迭代器對象飘哨,可以依次遍歷Generator函數(shù)內(nèi)部的每一個(gè)狀態(tài)。

形式上尼啡,Generator 函數(shù)是一個(gè)普通函數(shù)暂衡,但是有兩個(gè)特征。一是崖瞭,function關(guān)鍵字與函數(shù)名之間有一個(gè)星號狂巢;二是,函數(shù)體內(nèi)部使用yield表達(dá)式书聚,定義不同的內(nèi)部狀態(tài)(yield在英語里的意思就是“產(chǎn)出”)唧领。

function* demoGenerator() {
    yeild 'hello';
  yeild 'es6';
  return 'ending';
}

var hero = demoGenerator();

注意: es6語法中并未規(guī)定function關(guān)鍵字與函數(shù)名稱之間的*號應(yīng)該寫在哪個(gè)位置。

yield表達(dá)式

生成器函數(shù)返回的迭代器對象雌续,只有在調(diào)用next方法時(shí)才會遍歷下一個(gè)內(nèi)部狀態(tài)斩个,其實(shí)是因?yàn)樘峁┝艘环N可以暫停執(zhí)行的函數(shù)yield表達(dá)式就是暫停標(biāo)志。

該表達(dá)式只能用在生成器函數(shù)中西雀,用在其他地方都會報(bào)錯(cuò)萨驶。yield表達(dá)式如果用在另一個(gè)表達(dá)式中,必須放在圓括號里艇肴,而用作函數(shù)參數(shù)或放在賦值表達(dá)式的右邊腔呜,可以不加括號。

與迭代器接口的關(guān)系

由于生成器是迭代器的生成函數(shù)再悼,因此可以把Generator賦值給對象的Symbol.iterator屬性核畴,從而使得該對象具有迭代器接口

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

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

Generator 函數(shù)執(zhí)行后,返回一個(gè)迭代器對象冲九。該對象本身也具有Symbol.iterator屬性谤草,執(zhí)行后返回自身。

function* gen(){
  // some code
}

var g = gen();

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

上面代碼中莺奸,gen是一個(gè) Generator 函數(shù)丑孩,調(diào)用它會生成一個(gè)遍歷器對象g。它的Symbol.iterator屬性灭贷,也是一個(gè)遍歷器對象生成函數(shù)温学,執(zhí)行后返回它自己。

next方法的參數(shù)

yield表達(dá)式本身沒有返回值甚疟,或者說總是返回undefined仗岖。next方法可以帶一個(gè)參數(shù),該參數(shù)就會被當(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會等于-1是嗜,下一輪循環(huán)就會從-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è)next方法用來啟動迭代器對象,所以不用帶參數(shù)果正,如果第一次調(diào)用時(shí)炎码,就能夠輸入值盟迟,可以在生成器函數(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!

上面的例子中潦闲,生成器函數(shù)如果不用wrapper先包一層攒菠,是無法第一次調(diào)用next方法,就輸入?yún)?shù)的歉闰。

除了for...of循環(huán)以外(無需調(diào)用next方法)辖众,擴(kuò)展運(yùn)算符(...)、解構(gòu)賦值和Array.from方法內(nèi)部調(diào)用的和敬,都是遍歷器接口凹炸。這意味著,它們都可以將 Generator 函數(shù)返回的 Iterator 對象昼弟,作為參數(shù)啤它。

Generator.prototype.throw()

生成器函數(shù)返回的迭代對象,都有一個(gè)throw方法私杜,可以在函數(shù)體外拋出錯(cuò)誤蚕键,然后在生成器函數(shù)體內(nèi)捕獲。

throw方法可以接受一個(gè)參數(shù)衰粹,該參數(shù)會被catch語句接收锣光,建議拋出Error對象的實(shí)例。

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

var i = g();
i.next();
i.throw(new Error('出錯(cuò)了铝耻!'));
// Error: 出錯(cuò)了誊爹!(…)

注意,不要混淆遍歷器對象的throw方法和全局的throw命令瓢捉。上面代碼的錯(cuò)誤频丘,是用遍歷器對象的throw方法拋出的,而不是用throw命令拋出的泡态。后者只能被函數(shù)體外的catch語句捕獲搂漠。

如果 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代碼塊捕獲怔毛。之所以只捕獲了a,是因?yàn)楹瘮?shù)體外的catch語句塊腾降,捕獲了拋出的a錯(cuò)誤以后拣度,就不會再繼續(xù)try代碼塊里面剩余的語句了。

如果 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方法被捕獲以后辕录,會附帶執(zhí)行下一條yield表達(dá)式睦霎,也就是說附帶執(zhí)行一次next方法

Generator.prototype.return()

Generator函數(shù)返回的遍歷器對象,還有一個(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') || g.return() // { value: "foo", done: true } || { value: undefined, 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í),不提供參數(shù)诀浪,則返回值的value屬性為undefined棋返。

如果 Generator 函數(shù)內(nèi)部有try...finally代碼塊,那么return方法會推遲到finally代碼塊執(zhí)行完再執(zhí)行雷猪。

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月帝,是不會有效果的躏惋。

這個(gè)就需要用到yield*表達(dá)式,用來在一個(gè) Generator 函數(shù)里面執(zhí)行另一個(gè) Generator 函數(shù)嚷辅。

修改上述實(shí)例代碼:

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

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

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

從語法角度看簿姨,如果yield表達(dá)式后面跟的是一個(gè)遍歷器對象,需要在yield表達(dá)式后面加上星號,表明它返回的是一個(gè)遍歷器對象扁位。這被稱為yield*表達(dá)式准潭。

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

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

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

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

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

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

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

Generator 函數(shù)的this

生成器函數(shù)總是返回一個(gè)迭代器括改,es6規(guī)定這個(gè)迭代器是生成器函數(shù)的實(shí)例腻豌,也繼承了該函數(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的實(shí)例,而且繼承了g.prototype惹骂。但是苏携,如果把g當(dāng)作普通的構(gòu)造函數(shù),并不會生效析苫,因?yàn)?code>g返回的總是遍歷器對象兜叨,而不是this對象。

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

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

上面代碼中衩侥,Generator函數(shù)gthis對象上面添加了一個(gè)屬性a国旷,但是obj對象拿不到這個(gè)屬性。

Generator函數(shù)也不能跟new命令一起用茫死,會報(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è)正常的對象實(shí)例被环,既可以用next方法,又可以獲得正常的this详幽?

下面是一個(gè)變通方法筛欢。首先浸锨,生成一個(gè)空對象,使用call方法綁定 Generator 函數(shù)內(nèi)部的this版姑。這樣柱搜,構(gòu)造函數(shù)調(diào)用以后,這個(gè)空對象就是 Generator 函數(shù)的實(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)用它表制,返回一個(gè) Iterator 對象健爬。這個(gè)對象執(zhí)行三次next方法(因?yàn)?code>F內(nèi)部有兩個(gè)yield表達(dá)式),完成F內(nèi)部所有代碼的運(yùn)行夫凸。這時(shí)浑劳,所有內(nèi)部屬性都綁定在obj對象上了阱持,因此obj對象也就成了F的實(shí)例夭拌。

上面代碼中,執(zhí)行的是遍歷器對象f衷咽,但是生成的對象實(shí)例是obj鸽扁,有沒有辦法將這兩個(gè)對象統(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ù)桶现,就可以對它執(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

應(yīng)用

  • 異步操作的同步化表達(dá)
  • 控制流管理
  • 部署Iterator接口
  • 作為數(shù)據(jù)結(jié)構(gòu)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鼎姊,一起剝皮案震驚了整個(gè)濱河市骡和,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌相寇,老刑警劉巖慰于,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件唤衫,死亡現(xiàn)場離奇詭異婆赠,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)佳励,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門休里,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赃承,你說我怎么就攤上這事妙黍。” “怎么了瞧剖?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵拭嫁,是天一觀的道長。 經(jīng)常有香客問我,道長噩凹,這世上最難降的妖魔是什么巴元? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮驮宴,結(jié)果婚禮上逮刨,老公的妹妹穿的比我還像新娘。我一直安慰自己堵泽,他們只是感情好修己,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著迎罗,像睡著了一般睬愤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纹安,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天尤辱,我揣著相機(jī)與錄音,去河邊找鬼厢岂。 笑死光督,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的塔粒。 我是一名探鬼主播结借,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼卒茬!你這毒婦竟也來了船老?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤圃酵,失蹤者是張志新(化名)和其女友劉穎柳畔,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辜昵,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡荸镊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了堪置。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片躬存。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖舀锨,靈堂內(nèi)的尸體忽然破棺而出岭洲,到底是詐尸還是另有隱情,我是刑警寧澤坎匿,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布盾剩,位于F島的核電站雷激,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏告私。R本人自食惡果不足惜屎暇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望驻粟。 院中可真熱鬧根悼,春花似錦、人聲如沸蜀撑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酷麦。三九已至矿卑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間沃饶,已是汗流浹背母廷。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绍坝,地道東北人徘意。 一個(gè)月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像轩褐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子玖详,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348

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