在此處先列下本篇文章的主要內容
- 簡介
- next方法的參數
- for...of循環
- Generator.prototype.throw()
- Generator.prototype.return()
- yield* 語句
- 作為對象屬性的Generator函數
- Generator函數的this
- 含義
- 應用
簡介
基本概念
Generator 函數是 ES6 提供的一種異步編程解決方案,語法行為與傳統函數完全不同。本文詳細介紹Generator 函數的語法和 API,文章內容有點多,大家可以往下翻,只看自己關注的點。
Generator 函數有多種理解角度。從語法上,首先可以把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。
執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,還是一個遍歷器對象生成函數。返回的遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態。
形式上,Generator 函數是一個普通函數,但是有兩個特征。
- function關鍵字與函數名之間有一個星號;
- 函數體內部使用yield語句,定義不同的內部狀態;
如下所示:
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代碼定義了一個Generator函數helloWorldGenerator,它內部有兩個yield語句“hello”和“world”,即該函數有三個狀態:hello,world和return語句(結束執行)。
Generator函數的調用方法與普通函數一樣,也是在函數名后面加上一對圓括號。不同的是,調用Generator函數后,該函數并不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象(遍歷器對象)。
下一步,必須調用遍歷器對象的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
方法的運行邏輯如下。
- 遇到
yield
語句,就暫停執行后面的操作,并將緊跟在yield
后面的那個表達式的值,作為返回的對象的value
屬性值。
- 下一次調用
next
方法時,再繼續往下執行,直到遇到下一個yield
語句。 - 如果沒有再遇到新的
yield
語句,就一直運行到函數結束,直到return
語句為止,并將return
語句后面的表達式的值,作為返回的對象的value
屬性值。 - 如果該函數沒有
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;
})()
// Uncaught 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 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
方法,就輸入參數的。
再看一個通過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函數輸入值,然后打印出來。
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 (;;) {
[prev, curr] = [curr, prev + curr];
yield 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
方法被捕獲以后,會附帶執行下一條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() // { done: false, value: 1 }
g.next() // { done: false, value: 2 }
g.return(7) // { done: false, value: 4 }
g.next() // { done: false, value: 5 }
g.next() // { done: true, value: 7 }
上面代碼中,調用return
方法后,就開始執行finally
代碼塊,然后等到finally
代碼塊執行完,再執行return
方法。
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
的一種簡寫形式,完全可以用后者替代前者。反之,則需要用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.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)協程與普通線程的差異
不難看出,協程適合用于多任務運行的環境。在這個意義上,它與普通的線程很相似,都有自己的執行上下文、可以分享全局變量。它們的不同之處在于,同一時間可以有多個線程處于運行狀態,但是運行的協程只能有一個,其他協程都處于暫停狀態。此外,普通的線程是搶先式的,到底哪個線程優先得到資源,必須由運行環境決定,但是協程是合作式的,執行權由協程自己分配。
由于ECMAScript是單線程語言,只能保持一個調用棧。引入協程以后,每個任務可以保持自己的調用棧。這樣做的最大好處,就是拋出錯誤的時候,可以找到原始的調用棧。不至于像異步操作的回調函數那樣,一旦出錯,原始的調用棧早就結束。
Generator函數是ECMAScript 6對協程的實現,但屬于不完全實現。Generator函數被稱為“半協程”(semi-coroutine),意思是只有Generator函數的調用者,才能將程序的執行權還給Generator函數。如果是完全執行的協程,任何函數都可以讓暫停的協程繼續執行。
如果將Generator函數當作協程,完全可以將多個需要互相協作的任務寫成Generator函數,它們之間使用yield語句交換控制權。
應用
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界面,并且異步加載數據。等到數據加載完成,再一次使用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使得數據或者操作,具備了類似數組的接口。