生成器簡介
基本概念
生成器函數(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"
上面代碼中射沟,foo
和bar
都是 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ù)g
在this
對象上面添加了一個(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)