基本概念
Generator函數(shù)是ES6提供的一種針對異步編程的解決方案,它的語法和傳統(tǒng)的函數(shù)是不一樣的。
從語法上可以把Generator函數(shù)理解成狀態(tài)機,內(nèi)部封裝了多種狀態(tài)。同時,Generator函數(shù)在執(zhí)行時會返回一個遍歷器對象,依次遍歷函數(shù)中的每一個狀態(tài)。
function* helloGeneration(){
yield 'Hello';
yield 'world';
return 'ending'
}
let low = helloGeneration()
上面的代碼我們定義的一個Generation函數(shù),然后調(diào)用,但是調(diào)用不會和其他函數(shù)一樣直接執(zhí)行,因為它調(diào)用后返回的是一個指向內(nèi)部狀態(tài)的指針對象。
如果想要執(zhí)行,就必須用遍歷器接口的 next 方法,使得指針移到下一個狀態(tài)。也就是說,每次調(diào)用next方法,內(nèi)部指針會從函數(shù)頭部或上一次執(zhí)行的地方開始,直到遇到下一個yield表達式(或return語句)為止。換言之,Generator 函數(shù)是分段執(zhí)行的,yield表達式是暫停執(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 }
value屬性表示當(dāng)前的內(nèi)部狀態(tài)的值,是yield表達式后面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結(jié)束。
yield表達式
由于 Generator 函數(shù)返回的遍歷器對象,只有調(diào)用next方法才會遍歷下一個內(nèi)部狀態(tài),所以其實提供了一種可以暫停執(zhí)行的函數(shù)。yield表達式就是暫停標(biāo)志。
遍歷器對象的next方法的運行邏輯如下。
(1)遇到y(tǒng)ield表達式,就暫停執(zhí)行后面的操作,并將緊跟在yield后面的那個表達式的值,作為返回的對象的value屬性值。
(2)下一次調(diào)用next方法時,再繼續(xù)往下執(zhí)行,直到遇到下一個yield表達式。
(3)如果沒有再遇到新的yield表達式,就一直運行到函數(shù)結(jié)束,直到return語句為止,并將return語句后面的表達式的值,作為返回的對象的value屬性值。
(4)如果該函數(shù)沒有return語句,則返回的對象的value屬性值為undefined。
需要注意的是,yield表達式后面的表達式,只有當(dāng)調(diào)用next方法、內(nèi)部指針指向該語句時才會執(zhí)行,因此等于為 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能。
yield和return的異同
1、相同之處在于都能返回緊跟后面的表達式的值。
2、區(qū)別在于每次遇到y(tǒng)ield,函數(shù)暫停執(zhí)行,下一次再從該位置繼續(xù)向下執(zhí)行。而return語句不具備這種記憶功能,而且一個函數(shù)中只能出現(xiàn)一個return,而且之后退出執(zhí)行。Generator中可以返回多個yield。
3、需要注意的是yield只能在generator函數(shù)中用,否則會報錯。
具有 Iterator 接口,可以通過for...of方法是可以把所有的yield的值遍歷出來的
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);//遞歸回調(diào)
} else {
yield item;
}
}
};
for (var f of flat(arr)) {
console.log(f);//遍歷yield
}
// 1, 2, 3, 4, 5, 6
另外,yield表達式如果用在另一個表達式之中,必須放在圓括號里面。
next方法的參數(shù)問題
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ù),每次運行到y(tǒng)ield表達式,變量reset的值總是undefined。當(dāng)next方法帶一個參數(shù)true時,變量reset就被重置為這個參數(shù)(即true),因此i會等于-1,下一輪循環(huán)就會從-1開始遞增。
這個功能有很重要的語法意義。Generator 函數(shù)從暫停狀態(tài)到恢復(fù)運行,它的上下文狀態(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ù),導(dǎo)致 y 的值等于2 * undefined(即NaN),除以 3 以后還是NaN,因此返回對象的value屬性也等于NaN。第三次運行Next方法的時候不帶參數(shù),所以z等于undefined,返回對象的value屬性等于5 + NaN + undefined,即NaN。
如果向next方法提供參數(shù),返回結(jié)果就完全不一樣了。上面代碼第一次調(diào)用b的next方法時,返回x+1的值6;第二次調(diào)用next方法,將上一次yield表達式的值設(shè)為12,因此y等于24,返回y / 3的值8;第三次調(diào)用next方法,將上一次yield表達式的值設(shè)為13,因此z等于13,這時x等于5,y等于24,所以return語句的值等于42。其實傳的值就付給了上一個yield。
注意,由于next方法的參數(shù)表示上一個yield表達式的返回值,所以在第一次使用next方法時,傳遞參數(shù)是無效的。V8 引擎直接忽略第一次使用next方法時的參數(shù),只有從第二次使用next方法開始,參數(shù)才是有效的。從語義上講,第一個next方法用來啟動遍歷器對象,所以不用帶有參數(shù)。
for...of循環(huán)
for..of循環(huán)自動遍歷 Generator 函數(shù)時生成的Iterator對象,且此時不再需要調(diào)用next方法。利用for...of循環(huán),可以寫出遍歷任意對象(object)的方法。原生的 JavaScript 對象沒有遍歷接口,無法使用for...of循環(huán),通過 Generator 函數(shù)為它加上這個接口,就可以用了。
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)之中。
除了for...of循環(huán)以外,擴展運算符(...)、解構(gòu)賦值和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]
// 解構(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.return()
generator函數(shù)返回的遍歷器對象還有一個rerurn方法,返回固定的值,并且終結(jié)generator的遍歷。
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 }