Generator 函數(shù)的語法
簡介
基本概念
Generator 函數(shù)是 ES6 提供的一種異步編程解決方案条舔,語法行為與傳統(tǒng)函數(shù)完全不同期虾。
Generator 函數(shù)有多種理解角度。語法上,首先可以把它理解成,Generator 函數(shù)是一個狀態(tài)機,封裝了多個內(nèi)部狀態(tài)秉扑。
執(zhí)行 Generator 函數(shù)會返回一個遍歷器對象,也就是說调限,Generator 函數(shù)除了狀態(tài)機舟陆,還是一個遍歷器對象生成函數(shù)。返回的遍歷器對象旧噪,可以依次遍歷 Generator 函數(shù)內(nèi)部的每一個狀態(tài)吨娜。
形式上,Generator 函數(shù)是一個普通函數(shù)淘钟,但是有兩個特征宦赠。一是,function
關鍵字與函數(shù)名之間有一個星號;二是勾扭,函數(shù)體內(nèi)部使用yield
表達式毡琉,定義不同的內(nèi)部狀態(tài)(yield
在英語里的意思就是“產(chǎn)出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代碼定義了一個 Generator 函數(shù)helloWorldGenerator
妙色,它內(nèi)部有兩個yield
表達式(hello
和world
)桅滋,即該函數(shù)有三個狀態(tài):hello,world 和 return 語句(結束執(zhí)行)身辨。
然后丐谋,Generator 函數(shù)的調(diào)用方法與普通函數(shù)一樣,也是在函數(shù)名后面加上一對圓括號煌珊。不同的是号俐,調(diào)用 Generator 函數(shù)后,該函數(shù)并不執(zhí)行定庵,返回的也不是函數(shù)運行結果吏饿,而是一個指向內(nèi)部狀態(tài)的指針對象,也就是上一章介紹的遍歷器對象(Iterator Object)蔬浙。
下一步猪落,必須調(diào)用遍歷器對象的next
方法,使得指針移向下一個狀態(tài)畴博。也就是說笨忌,每次調(diào)用next
方法,內(nèi)部指針就從函數(shù)頭部或上一次停下來的地方開始執(zhí)行绎晃,直到遇到下一個yield
表達式(或return
語句)為止蜜唾。換言之杂曲,Generator 函數(shù)是分段執(zhí)行的庶艾,yield
表達式是暫停執(zhí)行的標記,而next
方法可以恢復執(zhí)行擎勘。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
上面代碼一共調(diào)用了四次next
方法咱揍。
第一次調(diào)用,Generator 函數(shù)開始執(zhí)行棚饵,直到遇到第一個yield
表達式為止煤裙。next
方法返回一個對象,它的value
屬性就是當前yield
表達式的值hello
噪漾,done
屬性的值false
硼砰,表示遍歷還沒有結束。
第二次調(diào)用欣硼,Generator 函數(shù)從上次yield
表達式停下的地方题翰,一直執(zhí)行到下一個yield
表達式。next
方法返回的對象的value
屬性就是當前yield
表達式的值world
,done
屬性的值false
豹障,表示遍歷還沒有結束冯事。
第三次調(diào)用,Generator 函數(shù)從上次yield
表達式停下的地方血公,一直執(zhí)行到return
語句(如果沒有return
語句昵仅,就執(zhí)行到函數(shù)結束)。next
方法返回的對象的value
屬性累魔,就是緊跟在return
語句后面的表達式的值(如果沒有return
語句摔笤,則value
屬性的值為undefined
),done
屬性的值true
,表示遍歷已經(jīng)結束皮璧。
第四次調(diào)用逾一,此時 Generator 函數(shù)已經(jīng)運行完畢,next
方法返回對象的value
屬性為undefined
寞冯,done
屬性為true
。以后再調(diào)用next
方法晚伙,返回的都是這個值吮龄。
總結一下,調(diào)用 Generator 函數(shù)咆疗,返回一個遍歷器對象漓帚,代表 Generator 函數(shù)的內(nèi)部指針。以后午磁,每次調(diào)用遍歷器對象的next
方法尝抖,就會返回一個有著value
和done
兩個屬性的對象。value
屬性表示當前的內(nèi)部狀態(tài)的值迅皇,是yield
表達式后面那個表達式的值昧辽;done
屬性是一個布爾值,表示是否遍歷結束登颓。
ES6 沒有規(guī)定搅荞,function
關鍵字與函數(shù)名之間的星號,寫在哪個位置框咙。這導致下面的寫法都能通過咕痛。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
由于 Generator 函數(shù)仍然是普通函數(shù),所以一般的寫法是上面的第三種喇嘱,即星號緊跟在function
關鍵字后面茉贡。本書也采用這種寫法。
yield 表達式
由于 Generator 函數(shù)返回的遍歷器對象者铜,只有調(diào)用next
方法才會遍歷下一個內(nèi)部狀態(tài)腔丧,所以其實提供了一種可以暫停執(zhí)行的函數(shù)构蹬。yield
表達式就是暫停標志。
遍歷器對象的next
方法的運行邏輯如下悔据。
(1)遇到yield
表達式庄敛,就暫停執(zhí)行后面的操作,并將緊跟在yield
后面的那個表達式的值科汗,作為返回的對象的value
屬性值藻烤。
(2)下一次調(diào)用next
方法時,再繼續(xù)往下執(zhí)行头滔,直到遇到下一個yield
表達式怖亭。
(3)如果沒有再遇到新的yield
表達式,就一直運行到函數(shù)結束坤检,直到return
語句為止兴猩,并將return
語句后面的表達式的值,作為返回的對象的value
屬性值早歇。
(4)如果該函數(shù)沒有return
語句倾芝,則返回的對象的value
屬性值為undefined
。
需要注意的是箭跳,yield
表達式后面的表達式晨另,只有當調(diào)用next
方法、內(nèi)部指針指向該語句時才會執(zhí)行谱姓,因此等于為 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能借尿。
function* gen() {
yield 123 + 456;
}
上面代碼中,yield
后面的表達式123 + 456
屉来,不會立即求值路翻,只會在next
方法將指針移到這一句時,才會求值茄靠。
yield
表達式與return
語句既有相似之處茂契,也有區(qū)別。相似之處在于嘹黔,都能返回緊跟在語句后面的那個表達式的值账嚎。區(qū)別在于每次遇到yield
莫瞬,函數(shù)暫停執(zhí)行儡蔓,下一次再從該位置繼續(xù)向后執(zhí)行,而return
語句不具備位置記憶的功能疼邀。一個函數(shù)里面喂江,只能執(zhí)行一次(或者說一個)return
語句,但是可以執(zhí)行多次(或者說多個)yield
表達式旁振。正常函數(shù)只能返回一個值获询,因為只能執(zhí)行一次return
涨岁;Generator 函數(shù)可以返回一系列的值,因為可以有任意多個yield
吉嚣。從另一個角度看梢薪,也可以說 Generator 生成了一系列的值,這也就是它的名稱的來歷(英語中尝哆,generator 這個詞是“生成器”的意思)秉撇。
Generator 函數(shù)可以不用yield
表達式,這時就變成了一個單純的暫緩執(zhí)行函數(shù)秋泄。
function* f() {
console.log('執(zhí)行了琐馆!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面代碼中,函數(shù)f
如果是普通函數(shù)恒序,在為變量generator
賦值時就會執(zhí)行瘦麸。但是,函數(shù)f
是一個 Generator 函數(shù)歧胁,就變成只有調(diào)用next
方法時滋饲,函數(shù)f
才會執(zhí)行。
另外需要注意喊巍,yield
表達式只能用在 Generator 函數(shù)里面了赌,用在其他地方都會報錯。
(function (){
yield 1;
})()
// SyntaxError: Unexpected number
上面代碼在一個普通函數(shù)中使用yield
表達式玄糟,結果產(chǎn)生一個句法錯誤勿她。
下面是另外一個例子。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
a.forEach(function (item) {
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
});
};
for (var f of flat(arr)){
console.log(f);
}
上面代碼也會產(chǎn)生句法錯誤阵翎,因為forEach
方法的參數(shù)是一個普通函數(shù)逢并,但是在里面使用了yield
表達式(這個函數(shù)里面還使用了yield*
表達式,詳細介紹見后文)郭卫。一種修改方法是改用for
循環(huán)砍聊。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
var length = a.length;
for (var i = 0; i < length; i++) {
var item = a[i];
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
}
};
for (var f of flat(arr)) {
console.log(f);
}
// 1, 2, 3, 4, 5, 6
另外,yield
表達式如果用在另一個表達式之中贰军,必須放在圓括號里面玻蝌。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
yield
表達式用作函數(shù)參數(shù)或放在賦值表達式的右邊,可以不加括號词疼。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
與 Iterator 接口的關系
上一章說過俯树,任意一個對象的Symbol.iterator
方法,等于該對象的遍歷器生成函數(shù)贰盗,調(diào)用該函數(shù)會返回該對象的一個遍歷器對象许饿。
由于 Generator 函數(shù)就是遍歷器生成函數(shù),因此可以把 Generator 賦值給對象的Symbol.iterator
屬性舵盈,從而使得該對象具有 Iterator 接口陋率。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
上面代碼中球化,Generator 函數(shù)賦值給Symbol.iterator
屬性,從而使得myIterable
對象具有了 Iterator 接口瓦糟,可以被...
運算符遍歷了筒愚。
Generator 函數(shù)執(zhí)行后,返回一個遍歷器對象菩浙。該對象本身也具有Symbol.iterator
屬性锨能,執(zhí)行后返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
上面代碼中芍耘,gen
是一個 Generator 函數(shù)址遇,調(diào)用它會生成一個遍歷器對象g
。它的Symbol.iterator
屬性斋竞,也是一個遍歷器對象生成函數(shù)倔约,執(zhí)行后返回它自己。
next 方法的參數(shù)
yield
表達式本身沒有返回值坝初,或者說總是返回undefined
浸剩。next
方法可以帶一個參數(shù),該參數(shù)就會被當作上一個yield
表達式的返回值鳄袍。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
上面代碼先定義了一個可以無限運行的 Generator 函數(shù)f
绢要,如果next
方法沒有參數(shù),每次運行到yield
表達式拗小,變量reset
的值總是undefined
重罪。當next
方法帶一個參數(shù)true
時,變量reset
就被重置為這個參數(shù)(即true
)哀九,因此i
會等于-1
剿配,下一輪循環(huán)就會從-1
開始遞增。
這個功能有很重要的語法意義阅束。Generator 函數(shù)從暫停狀態(tài)到恢復運行呼胚,它的上下文狀態(tài)(context)是不變的。通過next
方法的參數(shù)息裸,就有辦法在 Generator 函數(shù)開始運行之后蝇更,繼續(xù)向函數(shù)體內(nèi)部注入值。也就是說呼盆,可以在 Generator 函數(shù)運行的不同階段年扩,從外部向內(nèi)部注入不同的值,從而調(diào)整函數(shù)行為宿亡。
再看一個例子常遂。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
上面代碼中,第二次運行next
方法的時候不帶參數(shù)挽荠,導致 y 的值等于2 * undefined
(即NaN
)克胳,除以 3 以后還是NaN
,因此返回對象的value
屬性也等于NaN
圈匆。第三次運行Next
方法的時候不帶參數(shù)漠另,所以z
等于undefined
,返回對象的value
屬性等于5 + NaN + undefined
跃赚,即NaN
笆搓。
如果向next
方法提供參數(shù),返回結果就完全不一樣了纬傲。上面代碼第一次調(diào)用b
的next
方法時满败,返回x+1
的值6
;第二次調(diào)用next
方法叹括,將上一次yield
表達式的值設為12
算墨,因此y
等于24
,返回y / 3
的值8
汁雷;第三次調(diào)用next
方法净嘀,將上一次yield
表達式的值設為13
,因此z
等于13
侠讯,這時x
等于5
挖藏,y
等于24
,所以return
語句的值等于42
厢漩。
注意膜眠,由于next
方法的參數(shù)表示上一個yield
表達式的返回值,所以在第一次使用next
方法時溜嗜,傳遞參數(shù)是無效的柴底。V8 引擎直接忽略第一次使用next
方法時的參數(shù),只有從第二次使用next
方法開始粱胜,參數(shù)才是有效的柄驻。從語義上講,第一個next
方法用來啟動遍歷器對象焙压,所以不用帶有參數(shù)鸿脓。
再看一個通過next
方法的參數(shù),向 Generator 函數(shù)內(nèi)部輸入值的例子涯曲。
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b
上面代碼是一個很直觀的例子野哭,每次通過next
方法向 Generator 函數(shù)輸入值,然后打印出來幻件。
如果想要第一次調(diào)用next
方法時拨黔,就能夠輸入值,可以在 Generator 函數(shù)外面再包一層绰沥。
function wrapper(generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args);
generatorObject.next();
return generatorObject;
};
}
const wrapped = wrapper(function* () {
console.log(`First input: ${yield}`);
return 'DONE';
});
wrapped().next('hello!')
// First input: hello!
上面代碼中篱蝇,Generator 函數(shù)如果不用wrapper
先包一層贺待,是無法第一次調(diào)用next
方法,就輸入?yún)?shù)的零截。
for...of 循環(huán)
for...of
循環(huán)可以自動遍歷 Generator 函數(shù)時生成的Iterator
對象麸塞,且此時不再需要調(diào)用next
方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
上面代碼使用for...of
循環(huán)涧衙,依次顯示 5 個yield
表達式的值哪工。這里需要注意,一旦next
方法的返回對象的done
屬性為true
弧哎,for...of
循環(huán)就會中止雁比,且不包含該返回對象,所以上面代碼的return
語句返回的6
撤嫩,不包括在for...of
循環(huán)之中偎捎。
下面是一個利用 Generator 函數(shù)和for...of
循環(huán),實現(xiàn)斐波那契數(shù)列的例子非洲。
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
for (let n of fibonacci()) {
if (n > 1000) break;
console.log(n);
}
從上面代碼可見鸭限,使用for...of
語句時不需要使用next
方法。
利用for...of
循環(huán)两踏,可以寫出遍歷任意對象(object)的方法败京。原生的 JavaScript 對象沒有遍歷接口,無法使用for...of
循環(huán)梦染,通過 Generator 函數(shù)為它加上這個接口赡麦,就可以用了。
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
上面代碼中帕识,對象jane
原生不具備 Iterator 接口泛粹,無法用for...of
遍歷。這時肮疗,我們通過 Generator 函數(shù)objectEntries
為它加上遍歷器接口晶姊,就可以用for...of
遍歷了。加上遍歷器接口的另一種寫法是伪货,將 Generator 函數(shù)加到對象的Symbol.iterator
屬性上面们衙。
function* objectEntries() {
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
除了for...of
循環(huán)以外,擴展運算符(...
)碱呼、解構賦值和Array.from
方法內(nèi)部調(diào)用的蒙挑,都是遍歷器接口。這意味著愚臀,它們都可以將 Generator 函數(shù)返回的 Iterator 對象忆蚀,作為參數(shù)。
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 擴展運算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解構賦值
let [x, y] = numbers();
x // 1
y // 2
// for...of 循環(huán)
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
Generator.prototype.throw()
Generator 函數(shù)返回的遍歷器對象,都有一個throw
方法馋袜,可以在函數(shù)體外拋出錯誤男旗,然后在 Generator 函數(shù)體內(nèi)捕獲。
var g = function* () {
try {
yield;
} catch (e) {
console.log('內(nèi)部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 內(nèi)部捕獲 a
// 外部捕獲 b
上面代碼中桃焕,遍歷器對象i
連續(xù)拋出兩個錯誤剑肯。第一個錯誤被 Generator 函數(shù)體內(nèi)的catch
語句捕獲捧毛。i
第二次拋出錯誤观堂,由于 Generator 函數(shù)內(nèi)部的catch
語句已經(jīng)執(zhí)行過了,不會再捕捉到這個錯誤了呀忧,所以這個錯誤就被拋出了 Generator 函數(shù)體师痕,被函數(shù)體外的catch
語句捕獲。
throw
方法可以接受一個參數(shù)而账,該參數(shù)會被catch
語句接收胰坟,建議拋出Error
對象的實例。
var g = function* () {
try {
yield;
} catch (e) {
console.log(e);
}
};
var i = g();
i.next();
i.throw(new Error('出錯了泞辐!'));
// Error: 出錯了笔横!(…)
注意,不要混淆遍歷器對象的throw
方法和全局的throw
命令咐吼。上面代碼的錯誤吹缔,是用遍歷器對象的throw
方法拋出的,而不是用throw
命令拋出的锯茄。后者只能被函數(shù)體外的catch
語句捕獲厢塘。
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;
console.log('內(nèi)部捕獲', e);
}
}
};
var i = g();
i.next();
try {
throw new Error('a');
throw new Error('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 外部捕獲 [Error: a]
上面代碼之所以只捕獲了a
,是因為函數(shù)體外的catch
語句塊肌幽,捕獲了拋出的a
錯誤以后晚碾,就不會再繼續(xù)try
代碼塊里面剩余的語句了。
如果 Generator 函數(shù)內(nèi)部沒有部署try...catch
代碼塊喂急,那么throw
方法拋出的錯誤格嘁,將被外部try...catch
代碼塊捕獲。
var g = function* () {
while (true) {
yield;
console.log('內(nèi)部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 外部捕獲 a
上面代碼中廊移,Generator 函數(shù)g
內(nèi)部沒有部署try...catch
代碼塊糕簿,所以拋出的錯誤直接被外部catch
代碼塊捕獲。
如果 Generator 函數(shù)內(nèi)部和外部画机,都沒有部署try...catch
代碼塊冶伞,那么程序?qū)箦e,直接中斷執(zhí)行步氏。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined
上面代碼中响禽,g.throw
拋出錯誤以后,沒有任何try...catch
代碼塊可以捕獲這個錯誤,導致程序報錯芋类,中斷執(zhí)行隆嗅。
throw
方法拋出的錯誤要被內(nèi)部捕獲,前提是必須至少執(zhí)行過一次next
方法侯繁。
function* gen() {
try {
yield 1;
} catch (e) {
console.log('內(nèi)部捕獲');
}
}
var g = gen();
g.throw(1);
// Uncaught 1
上面代碼中胖喳,g.throw(1)
執(zhí)行時,next
方法一次都沒有執(zhí)行過贮竟。這時丽焊,拋出的錯誤不會被內(nèi)部捕獲,而是直接在外部拋出咕别,導致程序出錯技健。這種行為其實很好理解,因為第一次執(zhí)行next
方法惰拱,等同于啟動執(zhí)行 Generator 函數(shù)的內(nèi)部代碼雌贱,否則 Generator 函數(shù)還沒有開始執(zhí)行,這時throw
方法拋錯只可能拋出在函數(shù)外部偿短。
throw
方法被捕獲以后欣孤,會附帶執(zhí)行下一條yield
表達式。也就是說昔逗,會附帶執(zhí)行一次next
方法降传。
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c
上面代碼中,g.throw
方法被捕獲以后纤子,自動執(zhí)行了一次next
方法搬瑰,所以會打印b
。另外控硼,也可以看到泽论,只要 Generator 函數(shù)內(nèi)部部署了try...catch
代碼塊,那么遍歷器的throw
方法拋出的錯誤卡乾,不影響下一次遍歷翼悴。
另外,throw
命令與g.throw
方法是無關的幔妨,兩者互不影響鹦赎。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
try {
throw new Error();
} catch (e) {
g.next();
}
// hello
// world
上面代碼中,throw
命令拋出的錯誤不會影響到遍歷器的狀態(tài)误堡,所以兩次執(zhí)行next
方法古话,都進行了正確的操作。
這種函數(shù)體內(nèi)捕獲錯誤的機制锁施,大大方便了對錯誤的處理陪踩。多個yield
表達式杖们,可以只用一個try...catch
代碼塊來捕獲錯誤。如果使用回調(diào)函數(shù)的寫法肩狂,想要捕獲多個錯誤摘完,就不得不為每個函數(shù)內(nèi)部寫一個錯誤處理語句,現(xiàn)在只在 Generator 函數(shù)內(nèi)部寫一次catch
語句就可以了傻谁。
Generator 函數(shù)體外拋出的錯誤孝治,可以在函數(shù)體內(nèi)捕獲;反過來审磁,Generator 函數(shù)體內(nèi)拋出的錯誤谈飒,也可以被函數(shù)體外的catch
捕獲。
function* foo() {
var x = yield 3;
var y = x.toUpperCase();
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err);
}
上面代碼中力图,第二個next
方法向函數(shù)體內(nèi)傳入一個參數(shù) 42步绸,數(shù)值是沒有toUpperCase
方法的掺逼,所以會拋出一個 TypeError 錯誤吃媒,被函數(shù)體外的catch
捕獲。
一旦 Generator 執(zhí)行過程中拋出錯誤吕喘,且沒有被內(nèi)部捕獲赘那,就不會再執(zhí)行下去了窥浪。如果此后還調(diào)用next
方法仰猖,將返回一個value
屬性等于undefined
撞反、done
屬性等于true
的對象纬纪,即 JavaScript 引擎認為這個 Generator 已經(jīng)運行結束了绎秒。
function* g() {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
yield 2;
yield 3;
}
function log(generator) {
var v;
console.log('starting generator');
try {
v = generator.next();
console.log('第一次運行next方法', v);
} catch (err) {
console.log('捕捉錯誤', v);
}
try {
v = generator.next();
console.log('第二次運行next方法', v);
} catch (err) {
console.log('捕捉錯誤', v);
}
try {
v = generator.next();
console.log('第三次運行next方法', v);
} catch (err) {
console.log('捕捉錯誤', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次運行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉錯誤 { value: 1, done: false }
// 第三次運行next方法 { value: undefined, done: true }
// caller done
上面代碼一共三次運行next
方法,第二次運行的時候會拋出錯誤钉嘹,然后第三次運行的時候跋涣,Generator 函數(shù)就已經(jīng)結束了,不再執(zhí)行下去了奖年。
Generator.prototype.return()
Generator 函數(shù)返回的遍歷器對象,還有一個return
方法沛贪,可以返回給定的值陋守,并且終結遍歷 Generator 函數(shù)揍堰。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
上面代碼中,遍歷器對象g
調(diào)用return
方法后嗅义,返回值的value
屬性就是return
方法的參數(shù)foo
屏歹。并且,Generator 函數(shù)的遍歷就終止了之碗,返回值的done
屬性為true
蝙眶,以后再調(diào)用next
方法,done
屬性總是返回true
褪那。
如果return
方法調(diào)用時幽纷,不提供參數(shù),則返回值的value
屬性為undefined
博敬。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return() // { value: undefined, done: true }
如果 Generator 函數(shù)內(nèi)部有try...finally
代碼塊友浸,那么return
方法會推遲到finally
代碼塊執(zhí)行完再執(zhí)行。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
上面代碼中偏窝,調(diào)用return
方法后收恢,就開始執(zhí)行finally
代碼塊,然后等到finally
代碼塊執(zhí)行完祭往,再執(zhí)行return
方法。
next()驮肉、throw()、return() 的共同點
next()
卵渴、throw()
、return()
這三個方法本質(zhì)上是同一件事瑟啃,可以放在一起理解。它們的作用都是讓 Generator 函數(shù)恢復執(zhí)行错负,并且使用不同的語句替換yield
表達式折联。
next()
是將yield
表達式替換成一個值。
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相當于將 let result = yield x + y
// 替換成 let result = 1;
上面代碼中清笨,第二個next(1)
方法就相當于將yield
表達式替換成一個值1
。如果next
方法沒有參數(shù)检号,就相當于替換成undefined
。
throw()
是將yield
表達式替換成一個throw
語句最仑。
gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
// 相當于將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯了'));
return()
是將yield
表達式替換成一個return
語句欲芹。
gen.return(2); // Object {value: 2, done: true}
// 相當于將 let result = yield x + y
// 替換成 let result = return 2;
yield* 表達式
如果在 Generator 函數(shù)內(nèi)部,調(diào)用另一個 Generator 函數(shù)浙宜,默認情況下是沒有效果的。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
foo();
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "y"
上面代碼中俗批,foo
和bar
都是 Generator 函數(shù),在bar
里面調(diào)用foo
臭觉,是不會有效果的。
這個就需要用到yield*
表達式什乙,用來在一個 Generator 函數(shù)里面執(zhí)行另一個 Generator 函數(shù)。
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
再來看一個對比的例子忆某。
function* inner() {
yield 'hello!';
}
function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一個遍歷器對象
gen.next().value // "close"
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
上面例子中,outer2
使用了yield*
,outer1
沒使用削锰。結果就是,outer1
返回一個遍歷器對象蛹稍,outer2
返回該遍歷器對象的內(nèi)部值。
從語法角度看厦酬,如果yield
表達式后面跟的是一個遍歷器對象昌讲,需要在yield
表達式后面加上星號,表明它返回的是一個遍歷器對象醋闭。這被稱為yield*
表達式证逻。
let delegatedIterator = (function* () {
yield 'Hello!';
yield 'Bye!';
}());
let delegatingIterator = (function* () {
yield 'Greetings!';
yield* delegatedIterator;
yield 'Ok, bye.';
}());
for(let value of delegatingIterator) {
console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."
上面代碼中,delegatingIterator
是代理者龙宏,delegatedIterator
是被代理者。由于yield* delegatedIterator
語句得到的值花吟,是一個遍歷器键菱,所以要用星號表示拭抬。運行結果就是使用一個遍歷器,遍歷了多個 Generator 函數(shù)算凿,有遞歸的效果婚夫。
yield*
后面的 Generator 函數(shù)(沒有return
語句時),等同于在 Generator 函數(shù)內(nèi)部时捌,部署一個for...of
循環(huán)。
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同于
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
上面代碼說明禽笑,yield*
后面的 Generator 函數(shù)(沒有return
語句時),不過是for...of
的一種簡寫形式蟀伸,完全可以用后者替代前者。反之迟蜜,在有return
語句時,則需要用var value = yield* iterator
的形式獲取return
語句的值畦戒。
如果yield*
后面跟著一個數(shù)組,由于數(shù)組原生支持遍歷器垃环,因此就會遍歷數(shù)組成員被济。
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
上面代碼中,yield
命令后面如果不加星號钮追,返回的是整個數(shù)組,加了星號就表示返回的是數(shù)組的遍歷器對象。
實際上甥角,任何數(shù)據(jù)結構只要有 Iterator 接口,就可以被yield*
遍歷当犯。
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"
上面代碼中驰凛,yield
表達式返回整個字符串,yield*
語句返回單個字符涌献。因為字符串具有 Iterator 接口,所以被yield*
遍歷卜壕。
如果被代理的 Generator 函數(shù)有return
語句,那么就可以向代理它的 Generator 函數(shù)返回數(shù)據(jù)侦锯。
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
上面代碼在第四次調(diào)用next
方法的時候亲桥,屏幕上會有輸出,這是因為函數(shù)foo
的return
語句悼凑,向函數(shù)bar
提供了返回值。
再看一個例子渔欢。
function* genFuncWithReturn() {
yield 'a';
yield 'b';
return 'The result';
}
function* logReturned(genObj) {
let result = yield* genObj;
console.log(result);
}
[...logReturned(genFuncWithReturn())]
// The result
// 值為 [ 'a', 'b' ]
上面代碼中,存在兩次遍歷瘟忱。第一次是擴展運算符遍歷函數(shù)logReturned
返回的遍歷器對象奥额,第二次是yield*
語句遍歷函數(shù)genFuncWithReturn
返回的遍歷器對象。這兩次遍歷的效果是疊加的访诱,最終表現(xiàn)為擴展運算符遍歷函數(shù)genFuncWithReturn
返回的遍歷器對象垫挨。所以,最后的數(shù)據(jù)表達式得到的值等于[ 'a', 'b' ]
。但是,函數(shù)genFuncWithReturn
的return
語句的返回值The result
缰冤,會返回給函數(shù)logReturned
內(nèi)部的result
變量迂苛,因此會有終端輸出。
yield*
命令可以很方便地取出嵌套數(shù)組的所有成員有缆。
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
下面是一個稍微復雜的例子逆害,使用yield*
語句遍歷完全二叉樹留储。
// 下面是二叉樹的構造函數(shù)帅矗,
// 三個參數(shù)分別是左樹、當前節(jié)點和右樹
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍歷函數(shù)篇裁。
// 由于返回的是一個遍歷器产还,所以要用generator函數(shù)。
// 函數(shù)體內(nèi)采用遞歸算法际插,所以左樹和右樹要用yield*遍歷
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉樹
function make(array) {
// 判斷是否為葉節(jié)點
if (array.length == 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
// 遍歷二叉樹
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
作為對象屬性的 Generator 函數(shù)
如果一個對象的屬性是 Generator 函數(shù),可以簡寫成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
上面代碼中,myGeneratorMethod
屬性前面有一個星號,表示這個屬性是一個 Generator 函數(shù)科盛。
它的完整形式如下秫逝,與上面的寫法是等價的逊桦。
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
Generator 函數(shù)的this
Generator 函數(shù)總是返回一個遍歷器姓赤,ES6 規(guī)定這個遍歷器是 Generator 函數(shù)的實例,也繼承了 Generator 函數(shù)的prototype
對象上的方法涝开。
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
上面代碼表明核偿,Generator 函數(shù)g
返回的遍歷器obj
妇斤,是g
的實例荸恕,而且繼承了g.prototype
。但是死相,如果把g
當作普通的構造函數(shù)融求,并不會生效,因為g
返回的總是遍歷器對象算撮,而不是this
對象生宛。
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
上面代碼中县昂,Generator 函數(shù)g
在this
對象上面添加了一個屬性a
,但是obj
對象拿不到這個屬性茅糜。
Generator 函數(shù)也不能跟new
命令一起用七芭,會報錯。
function* F() {
yield this.x = 2;
yield this.y = 3;
}
new F()
// TypeError: F is not a constructor
上面代碼中蔑赘,new
命令跟構造函數(shù)F
一起使用狸驳,結果報錯,因為F
不是構造函數(shù)缩赛。
那么耙箍,有沒有辦法讓 Generator 函數(shù)返回一個正常的對象實例,既可以用next
方法酥馍,又可以獲得正常的this
辩昆?
下面是一個變通方法。首先旨袒,生成一個空對象汁针,使用call
方法綁定 Generator 函數(shù)內(nèi)部的this
。這樣砚尽,構造函數(shù)調(diào)用以后施无,這個空對象就是 Generator 函數(shù)的實例對象了。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
上面代碼中必孤,首先是F
內(nèi)部的this
對象綁定obj
對象猾骡,然后調(diào)用它,返回一個 Iterator 對象敷搪。這個對象執(zhí)行三次next
方法(因為F
內(nèi)部有兩個yield
表達式)兴想,完成 F 內(nèi)部所有代碼的運行。這時赡勘,所有內(nèi)部屬性都綁定在obj
對象上了嫂便,因此obj
對象也就成了F
的實例。
上面代碼中狮含,執(zhí)行的是遍歷器對象f
顽悼,但是生成的對象實例是obj
,有沒有辦法將這兩個對象統(tǒng)一呢几迄?
一個辦法就是將obj
換成F.prototype
蔚龙。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
再將F
改成構造函數(shù),就可以對它執(zhí)行new
命令了映胁。
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
含義
Generator 與狀態(tài)機
Generator 是實現(xiàn)狀態(tài)機的最佳結構木羹。比如,下面的clock
函數(shù)就是一個狀態(tài)機。
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}
上面代碼的clock
函數(shù)一共有兩種狀態(tài)(Tick
和Tock
)坑填,每運行一次抛人,就改變一次狀態(tài)。這個函數(shù)如果用 Generator 實現(xiàn)脐瑰,就是下面這樣妖枚。
var clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
上面的 Generator 實現(xiàn)與 ES5 實現(xiàn)對比,可以看到少了用來保存狀態(tài)的外部變量ticking
苍在,這樣就更簡潔绝页,更安全(狀態(tài)不會被非法篡改)、更符合函數(shù)式編程的思想寂恬,在寫法上也更優(yōu)雅续誉。Generator 之所以可以不用外部變量保存狀態(tài),是因為它本身就包含了一個狀態(tài)信息初肉,即目前是否處于暫停態(tài)酷鸦。
Generator 與協(xié)程
協(xié)程(coroutine)是一種程序運行的方式,可以理解成“協(xié)作的線程”或“協(xié)作的函數(shù)”牙咏。協(xié)程既可以用單線程實現(xiàn)臼隔,也可以用多線程實現(xiàn)。前者是一種特殊的子例程妄壶,后者是一種特殊的線程躬翁。
(1)協(xié)程與子例程的差異
傳統(tǒng)的“子例程”(subroutine)采用堆棧式“后進先出”的執(zhí)行方式,只有當調(diào)用的子函數(shù)完全執(zhí)行完畢盯拱,才會結束執(zhí)行父函數(shù)。協(xié)程與其不同例嘱,多個線程(單線程情況下狡逢,即多個函數(shù))可以并行執(zhí)行,但是只有一個線程(或函數(shù))處于正在運行的狀態(tài)拼卵,其他線程(或函數(shù))都處于暫停態(tài)(suspended)奢浑,線程(或函數(shù))之間可以交換執(zhí)行權。也就是說腋腮,一個線程(或函數(shù))執(zhí)行到一半雀彼,可以暫停執(zhí)行,將執(zhí)行權交給另一個線程(或函數(shù))即寡,等到稍后收回執(zhí)行權的時候徊哑,再恢復執(zhí)行。這種可以并行執(zhí)行聪富、交換執(zhí)行權的線程(或函數(shù))莺丑,就稱為協(xié)程。
從實現(xiàn)上看,在內(nèi)存中梢莽,子例程只使用一個棧(stack)萧豆,而協(xié)程是同時存在多個棧,但只有一個棧是在運行狀態(tài)昏名,也就是說涮雷,協(xié)程是以多占用內(nèi)存為代價,實現(xiàn)多任務的并行轻局。
(2)協(xié)程與普通線程的差異
不難看出洪鸭,協(xié)程適合用于多任務運行的環(huán)境。在這個意義上嗽交,它與普通的線程很相似卿嘲,都有自己的執(zhí)行上下文、可以分享全局變量夫壁。它們的不同之處在于拾枣,同一時間可以有多個線程處于運行狀態(tài),但是運行的協(xié)程只能有一個盒让,其他協(xié)程都處于暫停狀態(tài)梅肤。此外,普通的線程是搶先式的邑茄,到底哪個線程優(yōu)先得到資源姨蝴,必須由運行環(huán)境決定,但是協(xié)程是合作式的肺缕,執(zhí)行權由協(xié)程自己分配左医。
由于 JavaScript 是單線程語言,只能保持一個調(diào)用棧同木。引入?yún)f(xié)程以后浮梢,每個任務可以保持自己的調(diào)用棧。這樣做的最大好處彤路,就是拋出錯誤的時候秕硝,可以找到原始的調(diào)用棧。不至于像異步操作的回調(diào)函數(shù)那樣洲尊,一旦出錯远豺,原始的調(diào)用棧早就結束。
Generator 函數(shù)是 ES6 對協(xié)程的實現(xiàn)坞嘀,但屬于不完全實現(xiàn)躯护。Generator 函數(shù)被稱為“半?yún)f(xié)程”(semi-coroutine),意思是只有 Generator 函數(shù)的調(diào)用者丽涩,才能將程序的執(zhí)行權還給 Generator 函數(shù)榛做。如果是完全執(zhí)行的協(xié)程,任何函數(shù)都可以讓暫停的協(xié)程繼續(xù)執(zhí)行。
如果將 Generator 函數(shù)當作協(xié)程检眯,完全可以將多個需要互相協(xié)作的任務寫成 Generator 函數(shù)厘擂,它們之間使用yield
表達式交換控制權。
Generator 與上下文
JavaScript 代碼運行時锰瘸,會產(chǎn)生一個全局的上下文環(huán)境(context刽严,又稱運行環(huán)境),包含了當前所有的變量和對象避凝。然后舞萄,執(zhí)行函數(shù)(或塊級代碼)的時候,又會在當前上下文環(huán)境的上層管削,產(chǎn)生一個函數(shù)運行的上下文倒脓,變成當前(active)的上下文,由此形成一個上下文環(huán)境的堆棧(context stack)含思。
這個堆棧是“后進先出”的數(shù)據(jù)結構崎弃,最后產(chǎn)生的上下文環(huán)境首先執(zhí)行完成,退出堆棧含潘,然后再執(zhí)行完成它下層的上下文饲做,直至所有代碼執(zhí)行完成,堆棧清空遏弱。
Generator 函數(shù)不是這樣盆均,它執(zhí)行產(chǎn)生的上下文環(huán)境,一旦遇到yield
命令漱逸,就會暫時退出堆棧泪姨,但是并不消失,里面的所有變量和對象會凍結在當前狀態(tài)饰抒。等到對它執(zhí)行next
命令時驴娃,這個上下文環(huán)境又會重新加入調(diào)用棧,凍結的變量和對象恢復執(zhí)行循集。
function* gen() {
yield 1;
return 2;
}
let g = gen();
console.log(
g.next().value,
g.next().value,
);
上面代碼中,第一次執(zhí)行g.next()
時蔗草,Generator 函數(shù)gen
的上下文會加入堆棧咒彤,即開始運行gen
內(nèi)部的代碼。等遇到yield 1
時咒精,gen
上下文退出堆棧镶柱,內(nèi)部狀態(tài)凍結。第二次執(zhí)行g.next()
時模叙,gen
上下文重新加入堆棧歇拆,變成當前的上下文,重新恢復執(zhí)行。
應用
Generator 可以暫停函數(shù)執(zhí)行故觅,返回任意表達式的值厂庇。這種特點使得 Generator 有多種應用場景。
(1)異步操作的同步化表達
Generator 函數(shù)的暫停執(zhí)行的效果输吏,意味著可以把異步操作寫在yield
表達式里面权旷,等到調(diào)用next
方法時再往后執(zhí)行。這實際上等同于不需要寫回調(diào)函數(shù)了贯溅,因為異步操作的后續(xù)操作可以放在yield
表達式下面拄氯,反正要等到調(diào)用next
方法時再執(zhí)行。所以它浅,Generator 函數(shù)的一個重要實際意義就是用來處理異步操作译柏,改寫回調(diào)函數(shù)。
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加載UI
loader.next()
// 卸載UI
loader.next()
上面代碼中姐霍,第一次調(diào)用loadUI
函數(shù)時鄙麦,該函數(shù)不會執(zhí)行,僅返回一個遍歷器邮弹。下一次對該遍歷器調(diào)用next
方法黔衡,則會顯示Loading
界面(showLoadingScreen
),并且異步加載數(shù)據(jù)(loadUIDataAsynchronously
)腌乡。等到數(shù)據(jù)加載完成盟劫,再一次使用next
方法,則會隱藏Loading
界面与纽÷虑可以看到,這種寫法的好處是所有Loading
界面的邏輯急迂,都被封裝在一個函數(shù)影所,按部就班非常清晰。
Ajax 是典型的異步操作僚碎,通過 Generator 函數(shù)部署 Ajax 操作猴娩,可以用同步的方式表達。
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
var it = main();
it.next();
上面代碼的main
函數(shù)勺阐,就是通過 Ajax 操作獲取數(shù)據(jù)卷中。可以看到渊抽,除了多了一個yield
蟆豫,它幾乎與同步操作的寫法完全一樣。注意懒闷,makeAjaxCall
函數(shù)中的next
方法十减,必須加上response
參數(shù)栈幸,因為yield
表達式,本身是沒有值的帮辟,總是等于undefined
速址。
下面是另一個例子,通過 Generator 函數(shù)逐行讀取文本文件织阅。
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
上面代碼打開文本文件壳繁,使用yield
表達式可以手動逐行讀取文件。
(2)控制流管理
如果有一個多步操作非常耗時荔棉,采用回調(diào)函數(shù)闹炉,可能會寫成下面這樣。
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
采用 Promise 改寫上面的代碼润樱。
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done();
上面代碼已經(jīng)把回調(diào)函數(shù)渣触,改成了直線執(zhí)行的形式,但是加入了大量 Promise 的語法壹若。Generator 函數(shù)可以進一步改善代碼運行流程嗅钻。
function* longRunningTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
var value5 = yield step4(value4);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
然后,使用一個函數(shù)店展,按次序自動執(zhí)行所有步驟养篓。
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函數(shù)未結束,就繼續(xù)調(diào)用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
注意赂蕴,上面這種做法柳弄,只適合同步操作,即所有的task
都必須是同步的概说,不能有異步操作碧注。因為這里的代碼一得到返回值,就繼續(xù)往下執(zhí)行糖赔,沒有判斷異步操作何時完成萍丐。如果要控制異步的操作流程,詳見后面的《異步操作》一章放典。
下面逝变,利用for...of
循環(huán)會自動依次執(zhí)行yield
命令的特性,提供一種更一般的控制流管理的方法奋构。
let steps = [step1Func, step2Func, step3Func];
function* iterateSteps(steps){
for (var i=0; i< steps.length; i++){
var step = steps[i];
yield step();
}
}
上面代碼中壳影,數(shù)組steps
封裝了一個任務的多個步驟,Generator 函數(shù)iterateSteps
則是依次為這些步驟加上yield
命令声怔。
將任務分解成步驟之后,還可以將項目分解成多個依次執(zhí)行的任務舱呻。
let jobs = [job1, job2, job3];
function* iterateJobs(jobs){
for (var i=0; i< jobs.length; i++){
var job = jobs[i];
yield* iterateSteps(job.steps);
}
}
上面代碼中,數(shù)組jobs
封裝了一個項目的多個任務份名,Generator 函數(shù)iterateJobs
則是依次為這些任務加上yield*
命令贩据。
最后,就可以用for...of
循環(huán)一次性依次執(zhí)行所有任務的所有步驟柿冲。
for (var step of iterateJobs(jobs)){
console.log(step.id);
}
再次提醒,上面的做法只能用于所有步驟都是同步操作的情況兆旬,不能有異步操作的步驟假抄。如果想要依次執(zhí)行異步的步驟,必須使用后面的《異步操作》一章介紹的方法丽猬。
for...of
的本質(zhì)是一個while
循環(huán)宿饱,所以上面的代碼實質(zhì)上執(zhí)行的是下面的邏輯。
var it = iterateJobs(jobs);
var res = it.next();
while (!res.done){
var result = res.value;
// ...
res = it.next();
}
(3)部署 Iterator 接口
利用 Generator 函數(shù)脚祟,可以在任意對象上部署 Iterator 接口谬以。
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7
上述代碼中,myObj
是一個普通對象由桌,通過iterEntries
函數(shù)为黎,就有了 Iterator 接口。也就是說行您,可以在任意對象上部署next
方法铭乾。
下面是一個對數(shù)組部署 Iterator 接口的例子,盡管數(shù)組原生具有這個接口娃循。
function* makeSimpleGenerator(array){
var nextIndex = 0;
while(nextIndex < array.length){
yield array[nextIndex++];
}
}
var gen = makeSimpleGenerator(['yo', 'ya']);
gen.next().value // 'yo'
gen.next().value // 'ya'
gen.next().done // true
(4)作為數(shù)據(jù)結構
Generator 可以看作是數(shù)據(jù)結構炕檩,更確切地說,可以看作是一個數(shù)組結構淮野,因為 Generator 函數(shù)可以返回一系列的值捧书,這意味著它可以對任意表達式,提供類似數(shù)組的接口骤星。
function* doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
上面代碼就是依次返回三個函數(shù)经瓷,但是由于使用了 Generator 函數(shù),導致可以像處理數(shù)組那樣洞难,處理這三個返回的函數(shù)舆吮。
for (task of doStuff()) {
// task是一個函數(shù),可以像回調(diào)函數(shù)那樣使用它
}
實際上队贱,如果用 ES5 表達色冀,完全可以用數(shù)組模擬 Generator 的這種用法。
function doStuff() {
return [
fs.readFile.bind(null, 'hello.txt'),
fs.readFile.bind(null, 'world.txt'),
fs.readFile.bind(null, 'and-such.txt')
];
}
上面的函數(shù)柱嫌,可以用一模一樣的for...of
循環(huán)處理锋恬!兩相一比較,就不難看出 Generator 使得數(shù)據(jù)或者操作编丘,具備了類似數(shù)組的接口与学。