目錄
Generator 函數的語法
簡介
基本概念
Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數完全不同。
執行 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
表達式,結果產生一個句法錯誤。
另外,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
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
循環之中。
利用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
代碼塊,且正在執行try
代碼塊,那麼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
代碼塊,不執行try
裏面剩下的代碼了,然後等到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()
for (let i of foo()) {
yield i;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// x
// a
// b
// y
上面代碼中,foo
和bar
都是 Generator 函數,在bar
裏面調用foo
,就需要手動遍歷foo
。如果有多個 Generator 函數嵌套,寫起來就非常麻煩。
ES6 提供了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
由於擴展運算符...
默認調用 Iterator 接口,所以上面這個函數也可以用於嵌套數組的平鋪。
[...iterTree(tree)] // ["a", "b", "c", "d", "e"]
作爲對象屬性的 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 函數的異步應用
傳統方法
ES6 誕生以前,異步編程的方法,大概有下面四種。
- 回調函數
- 事件監聽
- 發佈/訂閱
- Promise 對象
Generator 函數將 JavaScript 異步編程帶入了一個全新的階段。
基本概念
異步
所謂"異步",簡單說就是一個任務不是連續完成的,可以理解成該任務被人爲分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。
比如,有一個任務是讀取文件進行處理,任務的第一段是向操作系統發出請求,要求讀取文件。然後,程序執行其他任務,等到操作系統返回文件,再接着執行任務的第二段(處理文件)。這種不連續的執行,就叫做異步。
相應地,連續的執行就叫做同步。由於是連續執行,不能插入其他任務,所以操作系統從硬盤讀取文件的這段時間,程序只能乾等着。
回調函數
JavaScript 語言對異步編程的實現,就是回調函數。所謂回調函數,就是把任務的第二段單獨寫在一個函數裏面,等到重新執行這個任務的時候,就直接調用這個函數。回調函數的英語名字callback
,直譯過來就是"重新調用"。
讀取文件進行處理,是這樣寫的。
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
if (err) throw err;
console.log(data);
});
上面代碼中,readFile
函數的第三個參數,就是回調函數,也就是任務的第二段。等到操作系統返回了/etc/passwd
這個文件以後,回調函數纔會執行。
Promise
回調函數本身並沒有問題,它的問題出現在多個回調函數嵌套。假定讀取A
文件之後,再讀取B
文件,代碼如下。
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
不難想象,如果依次讀取兩個以上的文件,就會出現多重嵌套。代碼不是縱向發展,而是橫向發展,很快就會亂成一團,無法管理。因爲多個異步操作形成了強耦合,只要有一個操作需要修改,它的上層回調函數和下層回調函數,可能都要跟着修改。這種情況就稱爲"回調函數地獄"(callback hell)。
Promise 對象就是爲了解決這個問題而提出的。它不是新的語法功能,而是一種新的寫法,允許將回調函數的嵌套,改成鏈式調用。採用 Promise,連續讀取多個文件,寫法如下。
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
上面代碼中,Promise 提供then
方法加載回調函數,catch
方法捕捉執行過程中拋出的錯誤。
可以看到,Promise 的寫法只是回調函數的改進,使用then
方法以後,異步任務的兩段執行看得更清楚了,除此以外,並無新意。
Promise 的最大問題是代碼冗餘,原來的任務被 Promise 包裝了一下,不管什麼操作,一眼看去都是一堆then
,原來的語義變得很不清楚。
那麼,有沒有更好的寫法呢?
Generator 函數
協程
傳統的編程語言,早有異步編程的解決方案(其實是多任務的解決方案)。其中有一種叫做"協程"(coroutine),意思是多個線程互相協作,完成異步任務。
協程有點像函數,又有點像線程。它的運行流程大致如下。
- 第一步,協程
A
開始執行。 - 第二步,協程
A
執行到一半,進入暫停,執行權轉移到協程B
。 - 第三步,(一段時間後)協程
B
交還執行權。 - 第四步,協程
A
恢復執行。
上面流程的協程A
,就是異步任務,因爲它分成兩段(或多段)執行。
舉例來說,讀取文件的協程寫法如下。
function* asyncJob() {
// ...其他代碼
var f = yield readFile(fileA);
// ...其他代碼
}
上面代碼的函數asyncJob
是一個協程,它的奧妙就在其中的yield
命令。它表示執行到此處,執行權將交給其他協程。也就是說,yield
命令是異步兩個階段的分界線。
協程遇到yield
命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行。它的最大優點,就是代碼的寫法非常像同步操作,如果去除yield
命令,簡直一模一樣。
協程的 Generator 函數實現
Generator 函數是協程在 ES6 的實現,最大特點就是可以交出函數的執行權(即暫停執行)。
整個 Generator 函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操作需要暫停的地方,都用yield
語句註明。Generator 函數的執行方法如下。
function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
上面代碼中,調用 Generator 函數,會返回一個內部指針(即遍歷器)g
。這是 Generator 函數不同於普通函數的另一個地方,即執行它不會返回結果,返回的是指針對象。調用指針g
的next
方法,會移動內部指針(即執行異步任務的第一段),指向第一個遇到的yield
語句,上例是執行到x + 2
爲止。
換言之,next
方法的作用是分階段執行Generator
函數。每次調用next
方法,會返回一個對象,表示當前階段的信息(value
屬性和done
屬性)。value
屬性是yield
語句後面表達式的值,表示當前階段的值;done
屬性是一個布爾值,表示 Generator 函數是否執行完畢,即是否還有下一個階段。
Generator 函數的數據交換和錯誤處理
Generator 函數可以暫停執行和恢復執行,這是它能封裝異步任務的根本原因。除此之外,它還有兩個特性,使它可以作爲異步編程的完整解決方案:函數體內外的數據交換和錯誤處理機制。
next
返回值的 value 屬性,是 Generator 函數向外輸出數據;next
方法還可以接受參數,向 Generator 函數體內輸入數據。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
上面代碼中,第一個next
方法的value
屬性,返回表達式x + 2
的值3
。第二個next
方法帶有參數2
,這個參數可以傳入 Generator 函數,作爲上個階段異步任務的返回結果,被函數體內的變量y
接收。因此,這一步的value
屬性,返回的就是2
(變量y
的值)。
Generator 函數內部還可以部署錯誤處理代碼,捕獲函數體外拋出的錯誤。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出錯了');
// 出錯了
上面代碼的最後一行,Generator 函數體外,使用指針對象的throw
方法拋出的錯誤,可以被函數體內的try...catch
代碼塊捕獲。這意味着,出錯的代碼與處理錯誤的代碼,實現了時間和空間上的分離,這對於異步編程無疑是很重要的。
異步任務的封裝
下面看看如何使用 Generator 函數,執行一個真實的異步任務。
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
上面代碼中,Generator 函數封裝了一個異步操作,該操作先讀取一個遠程接口,然後從 JSON 格式的數據解析信息。就像前面說過的,這段代碼非常像同步操作,除了加上了yield
命令。
執行這段代碼的方法如下。
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
上面代碼中,首先執行 Generator 函數,獲取遍歷器對象,然後使用next
方法(第二行),執行異步任務的第一階段。由於Fetch
模塊返回的是一個 Promise 對象,因此要用then
方法調用下一個next
方法。
可以看到,雖然 Generator 函數將異步操作表示得很簡潔,但是流程管理卻不方便(即何時執行第一階段、何時執行第二階段)。