Generator 函數的語法
簡介
基本概念
Generator 函數是 ES6 提供的一種異步編程解決方案,語法行為與傳統函數完全不同。
Generator 函數有多種理解角度。語法上,首先可以把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。
執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,還是一個遍歷器對象生成函數。返回的遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態。
形式上,Generator 函數是一個普通函數,但是有兩個特征。一是,function
關鍵字與函數名之間有一個星號;二是,函數體內部使用yield
表達式,定義不同的內部狀態(yield
在英語里的意思就是“產出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代碼定義了一個 Generator 函數helloWorldGenerator
,它內部有兩個yield
表達式(hello
和world
),即該函數有三個狀態:hello,world 和 return 語句(結束執行)。
然后,Generator 函數的調用方法與普通函數一樣,也是在函數名后面加上一對圓括號。不同的是,調用 Generator 函數后,該函數并不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象,也就是上一章介紹的遍歷器對象(Iterator Object)。
下一步,必須調用遍歷器對象的next
方法,使得指針移向下一個狀態。也就是說,每次調用next
方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield
表達式(或return
語句)為止。換言之,Generator 函數是分段執行的,yield
表達式是暫停執行的標記,而next
方法可以恢復執行。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
上面代碼一共調用了四次next
方法。
第一次調用,Generator 函數開始執行,直到遇到第一個yield
表達式為止。next
方法返回一個對象,它的value
屬性就是當前yield
表達式的值hello
,done
屬性的值false
,表示遍歷還沒有結束。
第二次調用,Generator 函數從上次yield
表達式停下的地方,一直執行到下一個yield
表達式。next
方法返回的對象的value
屬性就是當前yield
表達式的值world
,done
屬性的值false
,表示遍歷還沒有結束。
第三次調用,Generator 函數從上次yield
表達式停下的地方,一直執行到return
語句(如果沒有return
語句,就執行到函數結束)。next
方法返回的對象的value
屬性,就是緊跟在return
語句后面的表達式的值(如果沒有return
語句,則value
屬性的值為undefined
),done
屬性的值true
,表示遍歷已經結束。
第四次調用,此時 Generator 函數已經運行完畢,next
方法返回對象的value
屬性為undefined
,done
屬性為true
。以后再調用next
方法,返回的都是這個值。
總結一下,調用 Generator 函數,返回一個遍歷器對象,代表 Generator 函數的內部指針。以后,每次調用遍歷器對象的next
方法,就會返回一個有著value
和done
兩個屬性的對象。value
屬性表示當前的內部狀態的值,是yield
表達式后面那個表達式的值;done
屬性是一個布爾值,表示是否遍歷結束。
ES6 沒有規定,function
關鍵字與函數名之間的星號,寫在哪個位置。這導致下面的寫法都能通過。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
由于 Generator 函數仍然是普通函數,所以一般的寫法是上面的第三種,即星號緊跟在function
關鍵字后面。本書也采用這種寫法。
yield 表達式
由于 Generator 函數返回的遍歷器對象,只有調用next
方法才會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函數。yield
表達式就是暫停標志。
遍歷器對象的next
方法的運行邏輯如下。
(1)遇到yield
表達式,就暫停執行后面的操作,并將緊跟在yield
后面的那個表達式的值,作為返回的對象的value
屬性值。
(2)下一次調用next
方法時,再繼續往下執行,直到遇到下一個yield
表達式。
(3)如果沒有再遇到新的yield
表達式,就一直運行到函數結束,直到return
語句為止,并將return
語句后面的表達式的值,作為返回的對象的value
屬性值。
(4)如果該函數沒有return
語句,則返回的對象的value
屬性值為undefined
。
需要注意的是,yield
表達式后面的表達式,只有當調用next
方法、內部指針指向該語句時才會執行,因此等于為 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能。
function* gen() {
yield 123 + 456;
}
上面代碼中,yield
后面的表達式123 + 456
,不會立即求值,只會在next
方法將指針移到這一句時,才會求值。
yield
表達式與return
語句既有相似之處,也有區別。相似之處在于,都能返回緊跟在語句后面的那個表達式的值。區別在于每次遇到yield
,函數暫停執行,下一次再從該位置繼續向后執行,而return
語句不具備位置記憶的功能。一個函數里面,只能執行一次(或者說一個)return
語句,但是可以執行多次(或者說多個)yield
表達式。正常函數只能返回一個值,因為只能執行一次return
;Generator 函數可以返回一系列的值,因為可以有任意多個yield
。從另一個角度看,也可以說 Generator 生成了一系列的值,這也就是它的名稱的來歷(英語中,generator 這個詞是“生成器”的意思)。
Generator 函數可以不用yield
表達式,這時就變成了一個單純的暫緩執行函數。
function* f() {
console.log('執行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面代碼中,函數f
如果是普通函數,在為變量generator
賦值時就會執行。但是,函數f
是一個 Generator 函數,就變成只有調用next
方法時,函數f
才會執行。
另外需要注意,yield
表達式只能用在 Generator 函數里面,用在其他地方都會報錯。
(function (){
yield 1;
})()
// SyntaxError: Unexpected number
上面代碼在一個普通函數中使用yield
表達式,結果產生一個句法錯誤。
下面是另外一個例子。
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);
}
上面代碼也會產生句法錯誤,因為forEach
方法的參數是一個普通函數,但是在里面使用了yield
表達式(這個函數里面還使用了yield*
表達式,詳細介紹見后文)。一種修改方法是改用for
循環。
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
表達式用作函數參數或放在賦值表達式的右邊,可以不加括號。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
與 Iterator 接口的關系
上一章說過,任意一個對象的Symbol.iterator
方法,等于該對象的遍歷器生成函數,調用該函數會返回該對象的一個遍歷器對象。
由于 Generator 函數就是遍歷器生成函數,因此可以把 Generator 賦值給對象的Symbol.iterator
屬性,從而使得該對象具有 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
上面代碼中,Generator 函數賦值給Symbol.iterator
屬性,從而使得myIterable
對象具有了 Iterator 接口,可以被...
運算符遍歷了。
Generator 函數執行后,返回一個遍歷器對象。該對象本身也具有Symbol.iterator
屬性,執行后返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
上面代碼中,gen
是一個 Generator 函數,調用它會生成一個遍歷器對象g
。它的Symbol.iterator
屬性,也是一個遍歷器對象生成函數,執行后返回它自己。
next 方法的參數
yield
表達式本身沒有返回值,或者說總是返回undefined
。next
方法可以帶一個參數,該參數就會被當作上一個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 函數f
,如果next
方法沒有參數,每次運行到yield
表達式,變量reset
的值總是undefined
。當next
方法帶一個參數true
時,變量reset
就被重置為這個參數(即true
),因此i
會等于-1
,下一輪循環就會從-1
開始遞增。
這個功能有很重要的語法意義。Generator 函數從暫停狀態到恢復運行,它的上下文狀態(context)是不變的。通過next
方法的參數,就有辦法在 Generator 函數開始運行之后,繼續向函數體內部注入值。也就是說,可以在 Generator 函數運行的不同階段,從外部向內部注入不同的值,從而調整函數行為。
再看一個例子。
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
方法的時候不帶參數,導致 y 的值等于2 * undefined
(即NaN
),除以 3 以后還是NaN
,因此返回對象的value
屬性也等于NaN
。第三次運行Next
方法的時候不帶參數,所以z
等于undefined
,返回對象的value
屬性等于5 + NaN + undefined
,即NaN
。
如果向next
方法提供參數,返回結果就完全不一樣了。上面代碼第一次調用b
的next
方法時,返回x+1
的值6
;第二次調用next
方法,將上一次yield
表達式的值設為12
,因此y
等于24
,返回y / 3
的值8
;第三次調用next
方法,將上一次yield
表達式的值設為13
,因此z
等于13
,這時x
等于5
,y
等于24
,所以return
語句的值等于42
。
注意,由于next
方法的參數表示上一個yield
表達式的返回值,所以在第一次使用next
方法時,傳遞參數是無效的。V8 引擎直接忽略第一次使用next
方法時的參數,只有從第二次使用next
方法開始,參數才是有效的。從語義上講,第一個next
方法用來啟動遍歷器對象,所以不用帶有參數。
再看一個通過next
方法的參數,向 Generator 函數內部輸入值的例子。
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 函數輸入值,然后打印出來。
如果想要第一次調用next
方法時,就能夠輸入值,可以在 Generator 函數外面再包一層。
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 函數如果不用wrapper
先包一層,是無法第一次調用next
方法,就輸入參數的。
for...of 循環
for...of
循環可以自動遍歷 Generator 函數時生成的Iterator
對象,且此時不再需要調用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
循環,依次顯示 5 個yield
表達式的值。這里需要注意,一旦next
方法的返回對象的done
屬性為true
,for...of
循環就會中止,且不包含該返回對象,所以上面代碼的return
語句返回的6
,不包括在for...of
循環之中。
下面是一個利用 Generator 函數和for...of
循環,實現斐波那契數列的例子。
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
循環,可以寫出遍歷任意對象(object)的方法。原生的 JavaScript 對象沒有遍歷接口,無法使用for...of
循環,通過 Generator 函數為它加上這個接口,就可以用了。
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 函數objectEntries
為它加上遍歷器接口,就可以用for...of
遍歷了。加上遍歷器接口的另一種寫法是,將 Generator 函數加到對象的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
循環以外,擴展運算符(...
)、解構賦值和Array.from
方法內部調用的,都是遍歷器接口。這意味著,它們都可以將 Generator 函數返回的 Iterator 對象,作為參數。
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 循環
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
Generator.prototype.throw()
Generator 函數返回的遍歷器對象,都有一個throw
方法,可以在函數體外拋出錯誤,然后在 Generator 函數體內捕獲。
var g = function* () {
try {
yield;
} catch (e) {
console.log('內部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b
上面代碼中,遍歷器對象i
連續拋出兩個錯誤。第一個錯誤被 Generator 函數體內的catch
語句捕獲。i
第二次拋出錯誤,由于 Generator 函數內部的catch
語句已經執行過了,不會再捕捉到這個錯誤了,所以這個錯誤就被拋出了 Generator 函數體,被函數體外的catch
語句捕獲。
throw
方法可以接受一個參數,該參數會被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
命令拋出的。后者只能被函數體外的catch
語句捕獲。
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;
console.log('內部捕獲', e);
}
}
};
var i = g();
i.next();
try {
throw new Error('a');
throw new Error('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 外部捕獲 [Error: a]
上面代碼之所以只捕獲了a
,是因為函數體外的catch
語句塊,捕獲了拋出的a
錯誤以后,就不會再繼續try
代碼塊里面剩余的語句了。
如果 Generator 函數內部沒有部署try...catch
代碼塊,那么throw
方法拋出的錯誤,將被外部try...catch
代碼塊捕獲。
var g = function* () {
while (true) {
yield;
console.log('內部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 外部捕獲 a
上面代碼中,Generator 函數g
內部沒有部署try...catch
代碼塊,所以拋出的錯誤直接被外部catch
代碼塊捕獲。
如果 Generator 函數內部和外部,都沒有部署try...catch
代碼塊,那么程序將報錯,直接中斷執行。
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
代碼塊可以捕獲這個錯誤,導致程序報錯,中斷執行。
throw
方法拋出的錯誤要被內部捕獲,前提是必須至少執行過一次next
方法。
function* gen() {
try {
yield 1;
} catch (e) {
console.log('內部捕獲');
}
}
var g = gen();
g.throw(1);
// Uncaught 1
上面代碼中,g.throw(1)
執行時,next
方法一次都沒有執行過。這時,拋出的錯誤不會被內部捕獲,而是直接在外部拋出,導致程序出錯。這種行為其實很好理解,因為第一次執行next
方法,等同于啟動執行 Generator 函數的內部代碼,否則 Generator 函數還沒有開始執行,這時throw
方法拋錯只可能拋出在函數外部。
throw
方法被捕獲以后,會附帶執行下一條yield
表達式。也就是說,會附帶執行一次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
方法被捕獲以后,自動執行了一次next
方法,所以會打印b
。另外,也可以看到,只要 Generator 函數內部部署了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
命令拋出的錯誤不會影響到遍歷器的狀態,所以兩次執行next
方法,都進行了正確的操作。
這種函數體內捕獲錯誤的機制,大大方便了對錯誤的處理。多個yield
表達式,可以只用一個try...catch
代碼塊來捕獲錯誤。如果使用回調函數的寫法,想要捕獲多個錯誤,就不得不為每個函數內部寫一個錯誤處理語句,現在只在 Generator 函數內部寫一次catch
語句就可以了。
Generator 函數體外拋出的錯誤,可以在函數體內捕獲;反過來,Generator 函數體內拋出的錯誤,也可以被函數體外的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
方法向函數體內傳入一個參數 42,數值是沒有toUpperCase
方法的,所以會拋出一個 TypeError 錯誤,被函數體外的catch
捕獲。
一旦 Generator 執行過程中拋出錯誤,且沒有被內部捕獲,就不會再執行下去了。如果此后還調用next
方法,將返回一個value
屬性等于undefined
、done
屬性等于true
的對象,即 JavaScript 引擎認為這個 Generator 已經運行結束了。
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 函數就已經結束了,不再執行下去了。
Generator.prototype.return()
Generator 函數返回的遍歷器對象,還有一個return
方法,可以返回給定的值,并且終結遍歷 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
調用return
方法后,返回值的value
屬性就是return
方法的參數foo
。并且,Generator 函數的遍歷就終止了,返回值的done
屬性為true
,以后再調用next
方法,done
屬性總是返回true
。
如果return
方法調用時,不提供參數,則返回值的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 函數內部有try...finally
代碼塊,那么return
方法會推遲到finally
代碼塊執行完再執行。
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 }
上面代碼中,調用return
方法后,就開始執行finally
代碼塊,然后等到finally
代碼塊執行完,再執行return
方法。
next()、throw()、return() 的共同點
next()
、throw()
、return()
這三個方法本質上是同一件事,可以放在一起理解。它們的作用都是讓 Generator 函數恢復執行,并且使用不同的語句替換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
方法沒有參數,就相當于替換成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 函數內部,調用另一個 Generator 函數,默認情況下是沒有效果的。
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 函數,在bar
里面調用foo
,是不會有效果的。
這個就需要用到yield*
表達式,用來在一個 Generator 函數里面執行另一個 Generator 函數。
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
返回該遍歷器對象的內部值。
從語法角度看,如果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 函數,有遞歸的效果。
yield*
后面的 Generator 函數(沒有return
語句時),等同于在 Generator 函數內部,部署一個for...of
循環。
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 函數(沒有return
語句時),不過是for...of
的一種簡寫形式,完全可以用后者替代前者。反之,在有return
語句時,則需要用var value = yield* iterator
的形式獲取return
語句的值。
如果yield*
后面跟著一個數組,由于數組原生支持遍歷器,因此就會遍歷數組成員。
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
上面代碼中,yield
命令后面如果不加星號,返回的是整個數組,加了星號就表示返回的是數組的遍歷器對象。
實際上,任何數據結構只要有 Iterator 接口,就可以被yield*
遍歷。
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"
上面代碼中,yield
表達式返回整個字符串,yield*
語句返回單個字符。因為字符串具有 Iterator 接口,所以被yield*
遍歷。
如果被代理的 Generator 函數有return
語句,那么就可以向代理它的 Generator 函數返回數據。
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}
上面代碼在第四次調用next
方法的時候,屏幕上會有輸出,這是因為函數foo
的return
語句,向函數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' ]
上面代碼中,存在兩次遍歷。第一次是擴展運算符遍歷函數logReturned
返回的遍歷器對象,第二次是yield*
語句遍歷函數genFuncWithReturn
返回的遍歷器對象。這兩次遍歷的效果是疊加的,最終表現為擴展運算符遍歷函數genFuncWithReturn
返回的遍歷器對象。所以,最后的數據表達式得到的值等于[ 'a', 'b' ]
。但是,函數genFuncWithReturn
的return
語句的返回值The result
,會返回給函數logReturned
內部的result
變量,因此會有終端輸出。
yield*
命令可以很方便地取出嵌套數組的所有成員。
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*
語句遍歷完全二叉樹。
// 下面是二叉樹的構造函數,
// 三個參數分別是左樹、當前節點和右樹
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍歷函數。
// 由于返回的是一個遍歷器,所以要用generator函數。
// 函數體內采用遞歸算法,所以左樹和右樹要用yield*遍歷
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉樹
function make(array) {
// 判斷是否為葉節點
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 函數
如果一個對象的屬性是 Generator 函數,可以簡寫成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
上面代碼中,myGeneratorMethod
屬性前面有一個星號,表示這個屬性是一個 Generator 函數。
它的完整形式如下,與上面的寫法是等價的。
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
Generator 函數的this
Generator 函數總是返回一個遍歷器,ES6 規定這個遍歷器是 Generator 函數的實例,也繼承了 Generator 函數的prototype
對象上的方法。
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
上面代碼表明,Generator 函數g
返回的遍歷器obj
,是g
的實例,而且繼承了g.prototype
。但是,如果把g
當作普通的構造函數,并不會生效,因為g
返回的總是遍歷器對象,而不是this
對象。
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
上面代碼中,Generator 函數g
在this
對象上面添加了一個屬性a
,但是obj
對象拿不到這個屬性。
Generator 函數也不能跟new
命令一起用,會報錯。
function* F() {
yield this.x = 2;
yield this.y = 3;
}
new F()
// TypeError: F is not a constructor
上面代碼中,new
命令跟構造函數F
一起使用,結果報錯,因為F
不是構造函數。
那么,有沒有辦法讓 Generator 函數返回一個正常的對象實例,既可以用next
方法,又可以獲得正常的this
?
下面是一個變通方法。首先,生成一個空對象,使用call
方法綁定 Generator 函數內部的this
。這樣,構造函數調用以后,這個空對象就是 Generator 函數的實例對象了。
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
內部的this
對象綁定obj
對象,然后調用它,返回一個 Iterator 對象。這個對象執行三次next
方法(因為F
內部有兩個yield
表達式),完成 F 內部所有代碼的運行。這時,所有內部屬性都綁定在obj
對象上了,因此obj
對象也就成了F
的實例。
上面代碼中,執行的是遍歷器對象f
,但是生成的對象實例是obj
,有沒有辦法將這兩個對象統一呢?
一個辦法就是將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
改成構造函數,就可以對它執行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 與狀態機
Generator 是實現狀態機的最佳結構。比如,下面的clock
函數就是一個狀態機。
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}
上面代碼的clock
函數一共有兩種狀態(Tick
和Tock
),每運行一次,就改變一次狀態。這個函數如果用 Generator 實現,就是下面這樣。
var clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
上面的 Generator 實現與 ES5 實現對比,可以看到少了用來保存狀態的外部變量ticking
,這樣就更簡潔,更安全(狀態不會被非法篡改)、更符合函數式編程的思想,在寫法上也更優雅。Generator 之所以可以不用外部變量保存狀態,是因為它本身就包含了一個狀態信息,即目前是否處于暫停態。
Generator 與協程
協程(coroutine)是一種程序運行的方式,可以理解成“協作的線程”或“協作的函數”。協程既可以用單線程實現,也可以用多線程實現。前者是一種特殊的子例程,后者是一種特殊的線程。
(1)協程與子例程的差異
傳統的“子例程”(subroutine)采用堆棧式“后進先出”的執行方式,只有當調用的子函數完全執行完畢,才會結束執行父函數。協程與其不同,多個線程(單線程情況下,即多個函數)可以并行執行,但是只有一個線程(或函數)處于正在運行的狀態,其他線程(或函數)都處于暫停態(suspended),線程(或函數)之間可以交換執行權。也就是說,一個線程(或函數)執行到一半,可以暫停執行,將執行權交給另一個線程(或函數),等到稍后收回執行權的時候,再恢復執行。這種可以并行執行、交換執行權的線程(或函數),就稱為協程。
從實現上看,在內存中,子例程只使用一個棧(stack),而協程是同時存在多個棧,但只有一個棧是在運行狀態,也就是說,協程是以多占用內存為代價,實現多任務的并行。
(2)協程與普通線程的差異
不難看出,協程適合用于多任務運行的環境。在這個意義上,它與普通的線程很相似,都有自己的執行上下文、可以分享全局變量。它們的不同之處在于,同一時間可以有多個線程處于運行狀態,但是運行的協程只能有一個,其他協程都處于暫停狀態。此外,普通的線程是搶先式的,到底哪個線程優先得到資源,必須由運行環境決定,但是協程是合作式的,執行權由協程自己分配。
由于 JavaScript 是單線程語言,只能保持一個調用棧。引入協程以后,每個任務可以保持自己的調用棧。這樣做的最大好處,就是拋出錯誤的時候,可以找到原始的調用棧。不至于像異步操作的回調函數那樣,一旦出錯,原始的調用棧早就結束。
Generator 函數是 ES6 對協程的實現,但屬于不完全實現。Generator 函數被稱為“半協程”(semi-coroutine),意思是只有 Generator 函數的調用者,才能將程序的執行權還給 Generator 函數。如果是完全執行的協程,任何函數都可以讓暫停的協程繼續執行。
如果將 Generator 函數當作協程,完全可以將多個需要互相協作的任務寫成 Generator 函數,它們之間使用yield
表達式交換控制權。
Generator 與上下文
JavaScript 代碼運行時,會產生一個全局的上下文環境(context,又稱運行環境),包含了當前所有的變量和對象。然后,執行函數(或塊級代碼)的時候,又會在當前上下文環境的上層,產生一個函數運行的上下文,變成當前(active)的上下文,由此形成一個上下文環境的堆棧(context stack)。
這個堆棧是“后進先出”的數據結構,最后產生的上下文環境首先執行完成,退出堆棧,然后再執行完成它下層的上下文,直至所有代碼執行完成,堆棧清空。
Generator 函數不是這樣,它執行產生的上下文環境,一旦遇到yield
命令,就會暫時退出堆棧,但是并不消失,里面的所有變量和對象會凍結在當前狀態。等到對它執行next
命令時,這個上下文環境又會重新加入調用棧,凍結的變量和對象恢復執行。
function* gen() {
yield 1;
return 2;
}
let g = gen();
console.log(
g.next().value,
g.next().value,
);
上面代碼中,第一次執行g.next()
時,Generator 函數gen
的上下文會加入堆棧,即開始運行gen
內部的代碼。等遇到yield 1
時,gen
上下文退出堆棧,內部狀態凍結。第二次執行g.next()
時,gen
上下文重新加入堆棧,變成當前的上下文,重新恢復執行。
應用
Generator 可以暫停函數執行,返回任意表達式的值。這種特點使得 Generator 有多種應用場景。
(1)異步操作的同步化表達
Generator 函數的暫停執行的效果,意味著可以把異步操作寫在yield
表達式里面,等到調用next
方法時再往后執行。這實際上等同于不需要寫回調函數了,因為異步操作的后續操作可以放在yield
表達式下面,反正要等到調用next
方法時再執行。所以,Generator 函數的一個重要實際意義就是用來處理異步操作,改寫回調函數。
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加載UI
loader.next()
// 卸載UI
loader.next()
上面代碼中,第一次調用loadUI
函數時,該函數不會執行,僅返回一個遍歷器。下一次對該遍歷器調用next
方法,則會顯示Loading
界面(showLoadingScreen
),并且異步加載數據(loadUIDataAsynchronously
)。等到數據加載完成,再一次使用next
方法,則會隱藏Loading
界面。可以看到,這種寫法的好處是所有Loading
界面的邏輯,都被封裝在一個函數,按部就班非常清晰。
Ajax 是典型的異步操作,通過 Generator 函數部署 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
函數,就是通過 Ajax 操作獲取數據。可以看到,除了多了一個yield
,它幾乎與同步操作的寫法完全一樣。注意,makeAjaxCall
函數中的next
方法,必須加上response
參數,因為yield
表達式,本身是沒有值的,總是等于undefined
。
下面是另一個例子,通過 Generator 函數逐行讀取文本文件。
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
上面代碼打開文本文件,使用yield
表達式可以手動逐行讀取文件。
(2)控制流管理
如果有一個多步操作非常耗時,采用回調函數,可能會寫成下面這樣。
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();
上面代碼已經把回調函數,改成了直線執行的形式,但是加入了大量 Promise 的語法。Generator 函數可以進一步改善代碼運行流程。
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
}
}
然后,使用一個函數,按次序自動執行所有步驟。
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函數未結束,就繼續調用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
注意,上面這種做法,只適合同步操作,即所有的task
都必須是同步的,不能有異步操作。因為這里的代碼一得到返回值,就繼續往下執行,沒有判斷異步操作何時完成。如果要控制異步的操作流程,詳見后面的《異步操作》一章。
下面,利用for...of
循環會自動依次執行yield
命令的特性,提供一種更一般的控制流管理的方法。
let steps = [step1Func, step2Func, step3Func];
function* iterateSteps(steps){
for (var i=0; i< steps.length; i++){
var step = steps[i];
yield step();
}
}
上面代碼中,數組steps
封裝了一個任務的多個步驟,Generator 函數iterateSteps
則是依次為這些步驟加上yield
命令。
將任務分解成步驟之后,還可以將項目分解成多個依次執行的任務。
let jobs = [job1, job2, job3];
function* iterateJobs(jobs){
for (var i=0; i< jobs.length; i++){
var job = jobs[i];
yield* iterateSteps(job.steps);
}
}
上面代碼中,數組jobs
封裝了一個項目的多個任務,Generator 函數iterateJobs
則是依次為這些任務加上yield*
命令。
最后,就可以用for...of
循環一次性依次執行所有任務的所有步驟。
for (var step of iterateJobs(jobs)){
console.log(step.id);
}
再次提醒,上面的做法只能用于所有步驟都是同步操作的情況,不能有異步操作的步驟。如果想要依次執行異步的步驟,必須使用后面的《異步操作》一章介紹的方法。
for...of
的本質是一個while
循環,所以上面的代碼實質上執行的是下面的邏輯。
var it = iterateJobs(jobs);
var res = it.next();
while (!res.done){
var result = res.value;
// ...
res = it.next();
}
(3)部署 Iterator 接口
利用 Generator 函數,可以在任意對象上部署 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
函數,就有了 Iterator 接口。也就是說,可以在任意對象上部署next
方法。
下面是一個對數組部署 Iterator 接口的例子,盡管數組原生具有這個接口。
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)作為數據結構
Generator 可以看作是數據結構,更確切地說,可以看作是一個數組結構,因為 Generator 函數可以返回一系列的值,這意味著它可以對任意表達式,提供類似數組的接口。
function* doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
上面代碼就是依次返回三個函數,但是由于使用了 Generator 函數,導致可以像處理數組那樣,處理這三個返回的函數。
for (task of doStuff()) {
// task是一個函數,可以像回調函數那樣使用它
}
實際上,如果用 ES5 表達,完全可以用數組模擬 Generator 的這種用法。
function doStuff() {
return [
fs.readFile.bind(null, 'hello.txt'),
fs.readFile.bind(null, 'world.txt'),
fs.readFile.bind(null, 'and-such.txt')
];
}
上面的函數,可以用一模一樣的for...of
循環處理!兩相一比較,就不難看出 Generator 使得數據或者操作,具備了類似數組的接口。