1、簡(jiǎn)介
基本概念
Generator 函數(shù)是 ES6 提供的一種異步編程解決方案,語(yǔ)法行為與傳統(tǒng)函數(shù)完全不同。本章節(jié)詳細(xì)介紹 Generator 函數(shù)的語(yǔ)法和 API,它的異步編程應(yīng)用將在《Generator 函數(shù)的異步應(yīng)用》一節(jié)里。Generator 函數(shù)有多種理解角度。語(yǔ)法上,首先可以把它理解成,Generator 函數(shù)是一個(gè)狀態(tài)機(jī),封裝了多個(gè)內(nèi)部狀態(tài)。執(zhí)行 Generator 函數(shù)會(huì)返回一個(gè)遍歷器對(duì)象,也就是說(shuō),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在英語(yǔ)里的意思就是“產(chǎn)出”)。function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代碼定義了一個(gè) Generator 函數(shù)helloWorldGenerator,它內(nèi)部有兩個(gè)yield表達(dá)式(hello和world),即該函數(shù)有三個(gè)狀態(tài):hello,world 和 return 語(yǔ)句(結(jié)束執(zhí)行)。然后,Generator 函數(shù)的調(diào)用方法與普通函數(shù)一樣,也是在函數(shù)名后面加上一對(duì)圓括號(hào)。不同的是,調(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)。也就是說(shuō),每次調(diào)用next方法,內(nèi)部指針就從函數(shù)頭部或上一次停下來(lái)的地方開(kāi)始執(zhí)行,直到遇到下一個(gè)yield表達(dá)式(或return語(yǔ)句)為止。換言之,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)用,Generator 函數(shù)開(kāi)始執(zhí)行,直到遇到第一個(gè)yield表達(dá)式為止。next方法返回一個(gè)對(duì)象,它的value屬性就是當(dāng)前yield表達(dá)式的值hello,done屬性的值false,表示遍歷還沒(méi)有結(jié)束。第二次調(diào)用,Generator 函數(shù)從上次yield表達(dá)式停下的地方,一直執(zhí)行到下一個(gè)yield表達(dá)式。next方法返回的對(duì)象的value屬性就是當(dāng)前yield表達(dá)式的值world,done屬性的值false,表示遍歷還沒(méi)有結(jié)束。第三次調(diào)用,Generator 函數(shù)從上次yield表達(dá)式停下的地方,一直執(zhí)行到return語(yǔ)句(如果沒(méi)有return語(yǔ)句,就執(zhí)行到函數(shù)結(jié)束)。next方法返回的對(duì)象的value屬性,就是緊跟在return語(yǔ)句后面的表達(dá)式的值(如果沒(méi)有return語(yǔ)句,則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è)有著value和done兩個(gè)屬性的對(duì)象。value屬性表示當(dāng)前的內(nèi)部狀態(tài)的值,是yield表達(dá)式后面那個(gè)表達(dá)式的值;done屬性是一個(gè)布爾值,表示是否遍歷結(jié)束。ES6 沒(méi)有規(guī)定,function關(guān)鍵字與函數(shù)名之間的星號(hào),寫(xiě)在哪個(gè)位置。這導(dǎo)致下面的寫(xiě)法都能通過(guò)。function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
由于 Generator 函數(shù)仍然是普通函數(shù),所以一般的寫(xiě)法是上面的第三種,即星號(hào)緊跟在function關(guān)鍵字后面。本書(shū)也采用這種寫(xiě)法。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)遇到y(tǒng)ield表達(dá)式,就暫停執(zhí)行后面的操作,并將緊跟在yield后面的那個(gè)表達(dá)式的值,作為返回的對(duì)象的value屬性值。(2)下一次調(diào)用next方法時(shí),再繼續(xù)往下執(zhí)行,直到遇到下一個(gè)yield表達(dá)式。(3)如果沒(méi)有再遇到新的yield表達(dá)式,就一直運(yùn)行到函數(shù)結(jié)束,直到return語(yǔ)句為止,并將return語(yǔ)句后面的表達(dá)式的值,作為返回的對(duì)象的value屬性值。(4)如果該函數(shù)沒(méi)有return語(yǔ)句,則返回的對(duì)象的value屬性值為undefined。需要注意的是,yield表達(dá)式后面的表達(dá)式,只有當(dāng)調(diào)用next方法、內(nèi)部指針指向該語(yǔ)句時(shí)才會(huì)執(zhí)行,因此等于為 JavaScript 提供了手動(dòng)的“惰性求值”(Lazy Evaluation)的語(yǔ)法功能。function* gen() {
yield 123 + 456;
}
上面代碼中,yield后面的表達(dá)式123 + 456,不會(huì)立即求值,只會(huì)在next方法將指針移到這一句時(shí),才會(huì)求值。yield表達(dá)式與return語(yǔ)句既有相似之處,也有區(qū)別。相似之處在于,都能返回緊跟在語(yǔ)句后面的那個(gè)表達(dá)式的值。區(qū)別在于每次遇到y(tǒng)ield,函數(shù)暫停執(zhí)行,下一次再?gòu)脑撐恢美^續(xù)向后執(zhí)行,而return語(yǔ)句不具備位置記憶的功能。一個(gè)函數(shù)里面,只能執(zhí)行一次(或者說(shuō)一個(gè))return語(yǔ)句,但是可以執(zhí)行多次(或者說(shuō)多個(gè))yield表達(dá)式。正常函數(shù)只能返回一個(gè)值,因?yàn)橹荒軋?zhí)行一次return;Generator 函數(shù)可以返回一系列的值,因?yàn)榭梢杂腥我舛鄠€(gè)yield。從另一個(gè)角度看,也可以說(shuō) Generator 生成了一系列的值,這也就是它的名稱(chēng)的來(lái)歷(英語(yǔ)中,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ò)誤。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)閒orEach方法的參數(shù)是一個(gè)普通函數(shù),但是在里面使用了yield表達(dá)式(這個(gè)函數(shù)里面還使用了yield*表達(dá)式,詳細(xì)介紹見(jiàn)后文)。一種修改方法是改用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)系
上一章說(shuō)過(guò),任意一個(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í)行后返回它自己。2、next 方法的參數(shù)
yield表達(dá)式本身沒(méi)有返回值,或者說(shuō)總是返回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è)可以無(wú)限運(yùn)行的 Generator 函數(shù)f,如果next方法沒(méi)有參數(shù),每次運(yùn)行到y(tǒng)ield表達(dá)式,變量reset的值總是undefined。當(dāng)next方法帶一個(gè)參數(shù)true時(shí),變量reset就被重置為這個(gè)參數(shù)(即true),因此i會(huì)等于-1,下一輪循環(huán)就會(huì)從-1開(kāi)始遞增。這個(gè)功能有很重要的語(yǔ)法意義。Generator 函數(shù)從暫停狀態(tài)到恢復(fù)運(yùn)行,它的上下文狀態(tài)(context)是不變的。通過(guò)next方法的參數(shù),就有辦法在 Generator 函數(shù)開(kāi)始運(yùn)行之后,繼續(xù)向函數(shù)體內(nèi)部注入值。也就是說(shuō),可以在 Generator 函數(shù)運(yùn)行的不同階段,從外部向內(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 }
上面代碼中,第二次運(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)用b的next方法時(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語(yǔ)句的值等于42。注意,由于next方法的參數(shù)表示上一個(gè)yield表達(dá)式的返回值,所以在第一次使用next方法時(shí),傳遞參數(shù)是無(wú)效的。V8 引擎直接忽略第一次使用next方法時(shí)的參數(shù),只有從第二次使用next方法開(kāi)始,參數(shù)才是有效的。從語(yǔ)義上講,第一個(gè)next方法用來(lái)啟動(dòng)遍歷器對(duì)象,所以不用帶有參數(shù)。再看一個(gè)通過(guò)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è)很直觀的例子,每次通過(guò)next方法向 Generator 函數(shù)輸入值,然后打印出來(lái)。如果想要第一次調(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先包一層,是無(wú)法第一次調(diào)用next方法,就輸入?yún)?shù)的。3、for...of 循環(huán)
for...of循環(huán)可以自動(dòng)遍歷 Generator 函數(shù)運(yùn)行時(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語(yǔ)句返回的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);
}
從上面代碼可見(jiàn),使用for...of語(yǔ)句時(shí)不需要使用next方法。利用for...of循環(huán),可以寫(xiě)出遍歷任意對(duì)象(object)的方法。原生的 JavaScript 對(duì)象沒(méi)有遍歷接口,無(wú)法使用for...of循環(huán),通過(guò) 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 接口,無(wú)法用for...of遍歷。這時(shí),我們通過(guò) Generator 函數(shù)objectEntries為它加上遍歷器接口,就可以用for...of遍歷了。加上遍歷器接口的另一種寫(xiě)法是,將 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
4、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語(yǔ)句捕獲。i第二次拋出錯(cuò)誤,由于 Generator 函數(shù)內(nèi)部的catch語(yǔ)句已經(jīng)執(zhí)行過(guò)了,不會(huì)再捕捉到這個(gè)錯(cuò)誤了,所以這個(gè)錯(cuò)誤就被拋出了 Generator 函數(shù)體,被函數(shù)體外的catch語(yǔ)句捕獲。throw方法可以接受一個(gè)參數(shù),該參數(shù)會(huì)被catch語(yǔ)句接收,建議拋出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語(yǔ)句捕獲。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語(yǔ)句塊,捕獲了拋出的a錯(cuò)誤以后,就不會(huì)再繼續(xù)try代碼塊里面剩余的語(yǔ)句了。如果 Generator 函數(shù)內(nèi)部沒(mé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)部沒(méi)有部署try...catch代碼塊,所以拋出的錯(cuò)誤直接被外部catch代碼塊捕獲。如果 Generator 函數(shù)內(nèi)部和外部,都沒(mé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ò)誤以后,沒(méi)有任何try...catch代碼塊可以捕獲這個(gè)錯(cuò)誤,導(dǎo)致程序報(bào)錯(cuò),中斷執(zhí)行。throw方法拋出的錯(cuò)誤要被內(nèi)部捕獲,前提是必須至少執(zhí)行過(guò)一次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方法一次都沒(méi)有執(zhí)行過(guò)。這時(shí),拋出的錯(cuò)誤不會(huì)被內(nèi)部捕獲,而是直接在外部拋出,導(dǎo)致程序出錯(cuò)。這種行為其實(shí)很好理解,因?yàn)榈谝淮螆?zhí)行next方法,等同于啟動(dòng)執(zhí)行 Generator 函數(shù)的內(nèi)部代碼,否則 Generator 函數(shù)還沒(méi)有開(kāi)始執(zhí)行,這時(shí)throw方法拋錯(cuò)只可能拋出在函數(shù)外部。throw方法被捕獲以后,會(huì)附帶執(zhí)行下一條yield表達(dá)式。也就是說(shuō),會(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方法是無(wú)關(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),所以?xún)纱螆?zhí)行next方法,都進(jìn)行了正確的操作。這種函數(shù)體內(nèi)捕獲錯(cuò)誤的機(jī)制,大大方便了對(duì)錯(cuò)誤的處理。多個(gè)yield表達(dá)式,可以只用一個(gè)try...catch代碼塊來(lái)捕獲錯(cuò)誤。如果使用回調(diào)函數(shù)的寫(xiě)法,想要捕獲多個(gè)錯(cuò)誤,就不得不為每個(gè)函數(shù)內(nèi)部寫(xiě)一個(gè)錯(cuò)誤處理語(yǔ)句,現(xiàn)在只在 Generator 函數(shù)內(nèi)部寫(xiě)一次catch語(yǔ)句就可以了。Generator 函數(shù)體外拋出的錯(cuò)誤,可以在函數(shù)體內(nèi)捕獲;反過(guò)來(lá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ù)值是沒(méi)有toUpperCase方法的,所以會(huì)拋出一個(gè) TypeError 錯(cuò)誤,被函數(shù)體外的catch捕獲。一旦 Generator 執(zhí)行過(guò)程中拋出錯(cuò)誤,且沒(méi)有被內(nèi)部捕獲,就不會(huì)再執(zhí)行下去了。如果此后還調(diào)用next方法,將返回一個(gè)value屬性等于undefined、done屬性等于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í)行下去了。5、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代碼塊,且正在執(zhí)行try代碼塊,那么return方法會(huì)導(dǎo)致立刻進(jìn)入finally代碼塊,執(zhí)行完以后,整個(gè)函數(shù)才會(huì)結(jié)束。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()方法后,就開(kāi)始執(zhí)行finally代碼塊,不執(zhí)行try里面剩下的代碼了,然后等到finally代碼塊執(zhí)行完,再返回return()方法指定的返回值。6、next()、throw()、return() 的共同點(diǎn)
next()、throw()、return()這三個(gè)方法本質(zhì)上是同一件事,可以放在一起理解。它們的作用都是讓 Generator 函數(shù)恢復(fù)執(zhí)行,并且使用不同的語(yǔ)句替換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方法沒(méi)有參數(shù),就相當(dāng)于替換成undefined。throw()是將yield表達(dá)式替換成一個(gè)throw語(yǔ)句。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語(yǔ)句。gen.return(2); // Object {value: 2, done: true}
// 相當(dāng)于將 let result = yield x + y
// 替換成 let result = return 2;
7、yield* 表達(dá)式
如果在 Generator 函數(shù)內(nèi)部,調(diào)用另一個(gè) Generator 函數(shù)。需要在前者的函數(shù)體內(nèi)部,自己手動(dòng)完成遍歷。function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
// 手動(dòng)遍歷 foo()
for (let i of foo()) {
console.log(i);
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// x
// a
// b
// y
上面代碼中,foo和bar都是 Generator 函數(shù),在bar里面調(diào)用foo,就需要手動(dòng)遍歷foo。如果有多個(gè) Generator 函數(shù)嵌套,寫(xiě)起來(lái)就非常麻煩。ES6 提供了yield*表達(dá)式,作為解決辦法,用來(lái)在一個(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"
再來(lái)看一個(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沒(méi)使用。結(jié)果就是,outer1返回一個(gè)遍歷器對(duì)象,outer2返回該遍歷器對(duì)象的內(nèi)部值。從語(yǔ)法角度看,如果yield表達(dá)式后面跟的是一個(gè)遍歷器對(duì)象,需要在yield表達(dá)式后面加上星號(hào),表明它返回的是一個(gè)遍歷器對(duì)象。這被稱(chēng)為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語(yǔ)句得到的值,是一個(gè)遍歷器,所以要用星號(hào)表示。運(yùn)行結(jié)果就是使用一個(gè)遍歷器,遍歷了多個(gè) Generator 函數(shù),有遞歸的效果。yield*后面的 Generator 函數(shù)(沒(méi)有return語(yǔ)句時(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;
}
}
上面代碼說(shuō)明,yield*后面的 Generator 函數(shù)(沒(méi)有return語(yǔ)句時(shí)),不過(guò)是for...of的一種簡(jiǎn)寫(xiě)形式,完全可以用后者替代前者。反之,在有return語(yǔ)句時(shí),則需要用var value = yield* iterator的形式獲取return語(yǔ)句的值。如果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*語(yǔ)句返回單個(gè)字符。因?yàn)樽址哂?Iterator 接口,所以被yield*遍歷。如果被代理的 Generator 函數(shù)有return語(yǔ)句,那么就可以向代理它的 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ù)foo的return語(yǔ)句,向函數(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' ]
上面代碼中,存在兩次遍歷。第一次是擴(kuò)展運(yùn)算符遍歷函數(shù)logReturned返回的遍歷器對(duì)象,第二次是yield*語(yǔ)句遍歷函數(shù)genFuncWithReturn返回的遍歷器對(duì)象。這兩次遍歷的效果是疊加的,最終表現(xiàn)為擴(kuò)展運(yùn)算符遍歷函數(shù)genFuncWithReturn返回的遍歷器對(duì)象。所以,最后的數(shù)據(jù)表達(dá)式得到的值等于[ 'a', 'b' ]。但是,函數(shù)genFuncWithReturn的return語(yǔ)句的返回值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
由于擴(kuò)展運(yùn)算符...默認(rèn)調(diào)用 Iterator 接口,所以上面這個(gè)函數(shù)也可以用于嵌套數(shù)組的平鋪。[...iterTree(tree)] // ["a", "b", "c", "d", "e"]
下面是一個(gè)稍微復(fù)雜的例子,使用yield*語(yǔ)句遍歷完全二叉樹(shù)。// 下面是二叉樹(shù)的構(gòu)造函數(shù),
// 三個(gè)參數(shù)分別是左樹(shù)、當(dāng)前節(jié)點(diǎn)和右樹(shù)
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍歷函數(shù)。
// 由于返回的是一個(gè)遍歷器,所以要用generator函數(shù)。
// 函數(shù)體內(nèi)采用遞歸算法,所以左樹(shù)和右樹(shù)要用yield*遍歷
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉樹(shù)
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']]]);
// 遍歷二叉樹(shù)
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
8、作為對(duì)象屬性的 Generator 函數(shù)
如果一個(gè)對(duì)象的屬性是 Generator 函數(shù),可以簡(jiǎn)寫(xiě)成下面的形式。let obj = {
* myGeneratorMethod() {
···
}
};
上面代碼中,myGeneratorMethod屬性前面有一個(gè)星號(hào),表示這個(gè)屬性是一個(gè) Generator 函數(shù)。它的完整形式如下,與上面的寫(xiě)法是等價(jià)的。let obj = {
myGeneratorMethod: function* () {
// ···
}
};
9、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)間返回的總是遍歷器對(duì)象,而不是this對(duì)象。function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
上面代碼中,Generator 函數(shù)g在this對(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)镕不是構(gòu)造函數(shù)。那么,有沒(méi)有辦法讓 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)镕內(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,有沒(méi)有辦法將這兩個(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
10、含義
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)(Tick和Tock),每運(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ì)比,可以看到少了用來(lái)保存狀態(tài)的外部變量ticking,這樣就更簡(jiǎn)潔,更安全(狀態(tài)不會(huì)被非法篡改)、更符合函數(shù)式編程的思想,在寫(xiě)法上也更優(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)。前者是一種特殊的子例程,后者是一種特殊的線程。傳統(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)。也就是說(shuō),一個(gè)線程(或函數(shù))執(zhí)行到一半,可以暫停執(zhí)行,將執(zhí)行權(quán)交給另一個(gè)線程(或函數(shù)),等到稍后收回執(zhí)行權(quán)的時(shí)候,再恢復(fù)執(zhí)行。這種可以并行執(zhí)行、交換執(zhí)行權(quán)的線程(或函數(shù)),就稱(chēng)為協(xié)程。從實(shí)現(xiàn)上看,在內(nèi)存中,子例程只使用一個(gè)棧(stack),而協(xié)程是同時(shí)存在多個(gè)棧,但只有一個(gè)棧是在運(yùn)行狀態(tài),也就是說(shuō),協(xié)程是以多占用內(nèi)存為代價(jià),實(shí)現(xiàn)多任務(wù)的并行。不難看出,協(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 是單線程語(yǔ)言,只能保持一個(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ù)被稱(chēng)為“半?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ù)寫(xiě)成 Generator 函數(shù),它們之間使用yield表達(dá)式交換控制權(quán)。Generator 與上下文
JavaScript 代碼運(yùn)行時(shí),會(huì)產(chǎn)生一個(gè)全局的上下文環(huán)境(context,又稱(chēng)運(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)境,一旦遇到y(tǒng)ield命令,就會(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(shù).next()時(shí),Generator 函數(shù)gen的上下文會(huì)加入堆棧,即開(kāi)始運(yùn)行g(shù)en內(nèi)部的代碼。等遇到y(tǒng)ield 1時(shí),gen上下文退出堆棧,內(nèi)部狀態(tài)凍結(jié)。第二次執(zhí)行g(shù).next()時(shí),gen上下文重新加入堆棧,變成當(dāng)前的上下文,重新恢復(fù)執(zhí)行。11、應(yīng)用
Generator 可以暫停函數(shù)執(zhí)行,返回任意表達(dá)式的值。這種特點(diǎn)使得 Generator 有多種應(yīng)用場(chǎng)景。(1)異步操作的同步化表達(dá)
Generator 函數(shù)的暫停執(zhí)行的效果,意味著可以把異步操作寫(xiě)在yield表達(dá)式里面,等到調(diào)用next方法時(shí)再往后執(zhí)行。這實(shí)際上等同于不需要寫(xiě)回調(diào)函數(shù)了,因?yàn)楫惒讲僮鞯暮罄m(xù)操作可以放在yield表達(dá)式下面,反正要等到調(diào)用next方法時(shí)再執(zhí)行。所以,Generator 函數(shù)的一個(gè)重要實(shí)際意義就是用來(lái)處理異步操作,改寫(xiě)回調(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界面。可以看到,這種寫(xiě)法的好處是所有Loading界面的邏輯,都被封裝在一個(gè)函數(shù),按部就班非常清晰。Ajax 是典型的異步操作,通過(guò) 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ù),就是通過(guò) Ajax 操作獲取數(shù)據(jù)。可以看到,除了多了一個(gè)yield,它幾乎與同步操作的寫(xiě)法完全一樣。注意,makeAjaxCall函數(shù)中的next方法,必須加上response參數(shù),因?yàn)閥ield表達(dá)式,本身是沒(méi)有值的,總是等于undefined。下面是另一個(gè)例子,通過(guò) Generator 函數(shù)逐行讀取文本文件。function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
上面代碼打開(kāi)文本文件,使用yield表達(dá)式可以手動(dòng)逐行讀取文件。(2)控制流管理
如果有一個(gè)多步操作非常耗時(shí),采用回調(diào)函數(shù),可能會(huì)寫(xiě)成下面這樣。step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
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 的語(yǔ)法。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í)行,沒(méi)有判斷異步操作何時(shí)完成。如果要控制異步的操作流程,詳見(jiàn)后面的《異步操作》一章。下面,利用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ì)象,通過(guò)iterEntries函數(shù),就有了 Iterator 接口。也就是說(shuō),可以在任意對(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),更確切地說(shuō),可以看作是一個(gè)數(shù)組結(jié)構(gòu),因?yàn)?Generator 函數(shù)可以返回一系列的值,這意味著它可以對(duì)任意表達(dá)式,提供類(lèi)似數(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ù)或者操作,具備了類(lèi)似數(shù)組的接口。