Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣。本章詳細介紹 Generator 函數的語法和 API,它的異步編程應用請看《Generator 函數的異步應用》一章。javascript
Generator 函數有多種理解角度。語法上,首先能夠把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。java
執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,仍是一個遍歷器對象生成函數。返回的遍歷器對象,能夠依次遍歷 Generator 函數內部的每個狀態。node
形式上,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
,表示遍歷尚未結束。app
第三次調用,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
關鍵字後面。本書也採用這種寫法。
因爲 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 }
上一章說過,任意一個對象的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
屬性,也是一個遍歷器對象生成函數,執行後返回它本身。
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
循環能夠自動遍歷 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 函數返回的遍歷器對象,都有一個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 函數返回的遍歷器對象,還有一個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
代碼塊,而後等到finally
代碼塊執行完,再執行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;
若是在 Generator 函數內部,調用另外一個 Generator 函數。須要在前者的函數體內部,本身手動完成遍歷。
function* foo() { yield 'a'; yield 'b'; } function* bar() { yield 'x'; // 手動遍歷 foo() for (let i of foo()) { console.log(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"]
下面是一個稍微複雜的例子,使用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 函數,能夠簡寫成下面的形式。
let obj = { * myGeneratorMethod() { ··· } };
上面代碼中,myGeneratorMethod
屬性前面有一個星號,表示這個屬性是一個 Generator 函數。
它的完整形式以下,與上面的寫法是等價的。
let obj = { myGeneratorMethod: function* () { // ··· } };
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 是實現狀態機的最佳結構。好比,下面的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 之因此能夠不用外部變量保存狀態,是由於它自己就包含了一個狀態信息,即目前是否處於暫停態。
協程(coroutine)是一種程序運行的方式,能夠理解成「協做的線程」或「協做的函數」。協程既能夠用單線程實現,也能夠用多線程實現。前者是一種特殊的子例程,後者是一種特殊的線程。
(1)協程與子例程的差別
傳統的「子例程」(subroutine)採用堆棧式「後進先出」的執行方式,只有當調用的子函數徹底執行完畢,纔會結束執行父函數。協程與其不一樣,多個線程(單線程狀況下,即多個函數)能夠並行執行,可是隻有一個線程(或函數)處於正在運行的狀態,其餘線程(或函數)都處於暫停態(suspended),線程(或函數)之間能夠交換執行權。也就是說,一個線程(或函數)執行到一半,能夠暫停執行,將執行權交給另外一個線程(或函數),等到稍後收回執行權的時候,再恢復執行。這種能夠並行執行、交換執行權的線程(或函數),就稱爲協程。
從實現上看,在內存中,子例程只使用一個棧(stack),而協程是同時存在多個棧,但只有一個棧是在運行狀態,也就是說,協程是以多佔用內存爲代價,實現多任務的並行。
(2)協程與普通線程的差別
不難看出,協程適合用於多任務運行的環境。在這個意義上,它與普通的線程很類似,都有本身的執行上下文、能夠分享全局變量。它們的不一樣之處在於,同一時間能夠有多個線程處於運行狀態,可是運行的協程只能有一個,其餘協程都處於暫停狀態。此外,普通的線程是搶先式的,到底哪一個線程優先獲得資源,必須由運行環境決定,可是協程是合做式的,執行權由協程本身分配。
因爲 JavaScript 是單線程語言,只能保持一個調用棧。引入協程之後,每一個任務能夠保持本身的調用棧。這樣作的最大好處,就是拋出錯誤的時候,能夠找到原始的調用棧。不至於像異步操做的回調函數那樣,一旦出錯,原始的調用棧早就結束。
Generator 函數是 ES6 對協程的實現,但屬於不徹底實現。Generator 函數被稱爲「半協程」(semi-coroutine),意思是隻有 Generator 函數的調用者,才能將程序的執行權還給 Generator 函數。若是是徹底執行的協程,任何函數均可以讓暫停的協程繼續執行。
若是將 Generator 函數看成協程,徹底能夠將多個須要互相協做的任務寫成 Generator 函數,它們之間使用yield
表達式交換控制權。
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 有多種應用場景。
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
表達式能夠手動逐行讀取文件。
若是有一個多步操做很是耗時,採用回調函數,可能會寫成下面這樣。
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(); }
利用 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
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 使得數據或者操做,具有了相似數組的接口。