原文鏈接:http://es6.ruanyifeng.com/#docs/generator
內容有點多,還是有些不懂
簡介
基本概念
Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數完全不同。本章詳細介紹Generator 函數的語法和 API,它的異步編程應用請看《Generator 函數的異步應用》一章。
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語句不能用在普通函數中,否則會報錯。
(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語句如果用在一個表達式之中,必須放在圓括號裏面。
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
yield語句用作函數參數或賦值表達式的右邊,可以不加括號。
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使得數據或者操作,具備了類似數組的接口。