阮一峯老師的ES6入門:Generator 函數的語法

Generator 函數的語法

1. 簡介

基本概念

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表達式(helloworld),即該函數有三個狀態: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表達式的值hellodone屬性的值false,表示遍歷尚未結束。多線程

第二次調用,Generator 函數從上次yield表達式停下的地方,一直執行到下一個yield表達式。next方法返回的對象的value屬性就是當前yield表達式的值worlddone屬性的值false,表示遍歷尚未結束。app

第三次調用,Generator 函數從上次yield表達式停下的地方,一直執行到return語句(若是沒有return語句,就執行到函數結束)。next方法返回的對象的value屬性,就是緊跟在return語句後面的表達式的值(若是沒有return語句,則value屬性的值爲undefined),done屬性的值true,表示遍歷已經結束。

第四次調用,此時 Generator 函數已經運行完畢,next方法返回對象的value屬性爲undefineddone屬性爲true。之後再調用next方法,返回的都是這個值。

總結一下,調用 Generator 函數,返回一個遍歷器對象,表明 Generator 函數的內部指針。之後,每次調用遍歷器對象的next方法,就會返回一個有着valuedone兩個屬性的對象。value屬性表示當前的內部狀態的值,是yield表達式後面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。

ES6 沒有規定,function關鍵字與函數名之間的星號,寫在哪一個位置。這致使下面的寫法都能經過。

function * foo(x, y) { ··· } function *foo(x, y) { ··· } function* foo(x, y) { ··· } function*foo(x, y) { ··· } 

因爲 Generator 函數仍然是普通函數,因此通常的寫法是上面的第三種,即星號緊跟在function關鍵字後面。本書也採用這種寫法。

yield 表達式

因爲 Generator 函數返回的遍歷器對象,只有調用next方法纔會遍歷下一個內部狀態,因此其實提供了一種能夠暫停執行的函數。yield表達式就是暫停標誌。

遍歷器對象的next方法的運行邏輯以下。

(1)遇到yield表達式,就暫停執行後面的操做,並將緊跟在yield後面的那個表達式的值,做爲返回的對象的value屬性值。

(2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。

(3)若是沒有再遇到新的yield表達式,就一直運行到函數結束,直到return語句爲止,並將return語句後面的表達式的值,做爲返回的對象的value屬性值。

(4)若是該函數沒有return語句,則返回的對象的value屬性值爲undefined

須要注意的是,yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行,所以等於爲 JavaScript 提供了手動的「惰性求值」(Lazy Evaluation)的語法功能。

function* gen() { yield 123 + 456; } 

上面代碼中,yield後面的表達式123 + 456,不會當即求值,只會在next方法將指針移到這一句時,纔會求值。

yield表達式與return語句既有類似之處,也有區別。類似之處在於,都能返回緊跟在語句後面的那個表達式的值。區別在於每次遇到yield,函數暫停執行,下一次再從該位置繼續向後執行,而return語句不具有位置記憶的功能。一個函數裏面,只能執行一次(或者說一個)return語句,可是能夠執行屢次(或者說多個)yield表達式。正常函數只能返回一個值,由於只能執行一次return;Generator 函數能夠返回一系列的值,由於能夠有任意多個yield。從另外一個角度看,也能夠說 Generator 生成了一系列的值,這也就是它的名稱的來歷(英語中,generator 這個詞是「生成器」的意思)。

Generator 函數能夠不用yield表達式,這時就變成了一個單純的暫緩執行函數。

function* f() { console.log('執行了!') } var generator = f(); setTimeout(function () { generator.next() }, 2000); 

上面代碼中,函數f若是是普通函數,在爲變量generator賦值時就會執行。可是,函數f是一個 Generator 函數,就變成只有調用next方法時,函數f纔會執行。

另外須要注意,yield表達式只能用在 Generator 函數裏面,用在其餘地方都會報錯。

(function (){ yield 1; })() // SyntaxError: Unexpected number 

上面代碼在一個普通函數中使用yield表達式,結果產生一個句法錯誤。

下面是另一個例子。

var arr = [1, [[2, 3], 4], [5, 6]]; var flat = function* (a) { a.forEach(function (item) { if (typeof item !== 'number') { yield* flat(item); } else { yield item; } }); }; for (var f of flat(arr)){ console.log(f); } 

上面代碼也會產生句法錯誤,由於forEach方法的參數是一個普通函數,可是在裏面使用了yield表達式(這個函數裏面還使用了yield*表達式,詳細介紹見後文)。一種修改方法是改用for循環。

var arr = [1, [[2, 3], 4], [5, 6]]; var flat = function* (a) { var length = a.length; for (var i = 0; i < length; i++) { var item = a[i]; if (typeof item !== 'number') { yield* flat(item); } else { yield item; } } }; for (var f of flat(arr)) { console.log(f); } // 1, 2, 3, 4, 5, 6 

另外,yield表達式若是用在另外一個表達式之中,必須放在圓括號裏面。

function* demo() { console.log('Hello' + yield); // SyntaxError console.log('Hello' + yield 123); // SyntaxError console.log('Hello' + (yield)); // OK console.log('Hello' + (yield 123)); // OK } 

yield表達式用做函數參數或放在賦值表達式的右邊,能夠不加括號。

function* demo() { foo(yield 'a', yield 'b'); // OK let input = yield; // OK } 

與 Iterator 接口的關係

上一章說過,任意一個對象的Symbol.iterator方法,等於該對象的遍歷器生成函數,調用該函數會返回該對象的一個遍歷器對象。

因爲 Generator 函數就是遍歷器生成函數,所以能夠把 Generator 賦值給對象的Symbol.iterator屬性,從而使得該對象具備 Iterator 接口。

var myIterable = {}; myIterable[Symbol.iterator] = function* () { yield 1; yield 2; yield 3; }; [...myIterable] // [1, 2, 3] 

上面代碼中,Generator 函數賦值給Symbol.iterator屬性,從而使得myIterable對象具備了 Iterator 接口,能夠被...運算符遍歷了。

Generator 函數執行後,返回一個遍歷器對象。該對象自己也具備Symbol.iterator屬性,執行後返回自身。

function* gen(){  // some code } var g = gen(); g[Symbol.iterator]() === g // true 

上面代碼中,gen是一個 Generator 函數,調用它會生成一個遍歷器對象g。它的Symbol.iterator屬性,也是一個遍歷器對象生成函數,執行後返回它本身。

2. next 方法的參數

yield表達式自己沒有返回值,或者說老是返回undefinednext方法能夠帶一個參數,該參數就會被看成上一個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方法提供參數,返回結果就徹底不同了。上面代碼第一次調用bnext方法時,返回x+1的值6;第二次調用next方法,將上一次yield表達式的值設爲12,所以y等於24,返回y / 3的值8;第三次調用next方法,將上一次yield表達式的值設爲13,所以z等於13,這時x等於5y等於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方法,就輸入參數的。

3. 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屬性爲truefor...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 

4. 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屬性等於undefineddone屬性等於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 函數就已經結束了,再也不執行下去了。

5. 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代碼塊,而後等到finally代碼塊執行完,再執行return方法。

6. 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; 

7. yield* 表達式

若是在 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 

上面代碼中,foobar都是 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方法的時候,屏幕上會有輸出,這是由於函數fooreturn語句,向函數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' ]。可是,函數genFuncWithReturnreturn語句的返回值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'] 

8. 做爲對象屬性的 Generator 函數

若是一個對象的屬性是 Generator 函數,能夠簡寫成下面的形式。

let obj = { * myGeneratorMethod() { ··· } }; 

上面代碼中,myGeneratorMethod屬性前面有一個星號,表示這個屬性是一個 Generator 函數。

它的完整形式以下,與上面的寫法是等價的。

let obj = { myGeneratorMethod: function* () {  // ··· } }; 

9. 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 函數gthis對象上面添加了一個屬性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 

10. 含義

Generator 與狀態機

Generator 是實現狀態機的最佳結構。好比,下面的clock函數就是一個狀態機。

var ticking = true; var clock = function() { if (ticking) console.log('Tick!'); else console.log('Tock!'); ticking = !ticking; } 

上面代碼的clock函數一共有兩種狀態(TickTock),每運行一次,就改變一次狀態。這個函數若是用 Generator 實現,就是下面這樣。

var clock = function* () { while (true) { console.log('Tick!'); yield; console.log('Tock!'); yield; } }; 

上面的 Generator 實現與 ES5 實現對比,能夠看到少了用來保存狀態的外部變量ticking,這樣就更簡潔,更安全(狀態不會被非法篡改)、更符合函數式編程的思想,在寫法上也更優雅。Generator 之因此能夠不用外部變量保存狀態,是由於它自己就包含了一個狀態信息,即目前是否處於暫停態。

Generator 與協程

協程(coroutine)是一種程序運行的方式,能夠理解成「協做的線程」或「協做的函數」。協程既能夠用單線程實現,也能夠用多線程實現。前者是一種特殊的子例程,後者是一種特殊的線程。

(1)協程與子例程的差別

傳統的「子例程」(subroutine)採用堆棧式「後進先出」的執行方式,只有當調用的子函數徹底執行完畢,纔會結束執行父函數。協程與其不一樣,多個線程(單線程狀況下,即多個函數)能夠並行執行,可是隻有一個線程(或函數)處於正在運行的狀態,其餘線程(或函數)都處於暫停態(suspended),線程(或函數)之間能夠交換執行權。也就是說,一個線程(或函數)執行到一半,能夠暫停執行,將執行權交給另外一個線程(或函數),等到稍後收回執行權的時候,再恢復執行。這種能夠並行執行、交換執行權的線程(或函數),就稱爲協程。

從實現上看,在內存中,子例程只使用一個棧(stack),而協程是同時存在多個棧,但只有一個棧是在運行狀態,也就是說,協程是以多佔用內存爲代價,實現多任務的並行。

(2)協程與普通線程的差別

不難看出,協程適合用於多任務運行的環境。在這個意義上,它與普通的線程很類似,都有本身的執行上下文、能夠分享全局變量。它們的不一樣之處在於,同一時間能夠有多個線程處於運行狀態,可是運行的協程只能有一個,其餘協程都處於暫停狀態。此外,普通的線程是搶先式的,到底哪一個線程優先獲得資源,必須由運行環境決定,可是協程是合做式的,執行權由協程本身分配。

因爲 JavaScript 是單線程語言,只能保持一個調用棧。引入協程之後,每一個任務能夠保持本身的調用棧。這樣作的最大好處,就是拋出錯誤的時候,能夠找到原始的調用棧。不至於像異步操做的回調函數那樣,一旦出錯,原始的調用棧早就結束。

Generator 函數是 ES6 對協程的實現,但屬於不徹底實現。Generator 函數被稱爲「半協程」(semi-coroutine),意思是隻有 Generator 函數的調用者,才能將程序的執行權還給 Generator 函數。若是是徹底執行的協程,任何函數均可以讓暫停的協程繼續執行。

若是將 Generator 函數看成協程,徹底能夠將多個須要互相協做的任務寫成 Generator 函數,它們之間使用yield表達式交換控制權。

Generator 與上下文

JavaScript 代碼運行時,會產生一個全局的上下文環境(context,又稱運行環境),包含了當前全部的變量和對象。而後,執行函數(或塊級代碼)的時候,又會在當前上下文環境的上層,產生一個函數運行的上下文,變成當前(active)的上下文,由此造成一個上下文環境的堆棧(context stack)。

這個堆棧是「後進先出」的數據結構,最後產生的上下文環境首先執行完成,退出堆棧,而後再執行完成它下層的上下文,直至全部代碼執行完成,堆棧清空。

Generator 函數不是這樣,它執行產生的上下文環境,一旦遇到yield命令,就會暫時退出堆棧,可是並不消失,裏面的全部變量和對象會凍結在當前狀態。等到對它執行next命令時,這個上下文環境又會從新加入調用棧,凍結的變量和對象恢復執行。

function* gen() { yield 1; return 2; } let g = gen(); console.log( g.next().value, g.next().value, ); 

上面代碼中,第一次執行g.next()時,Generator 函數gen的上下文會加入堆棧,即開始運行gen內部的代碼。等遇到yield 1時,gen上下文退出堆棧,內部狀態凍結。第二次執行g.next()時,gen上下文從新加入堆棧,變成當前的上下文,從新恢復執行。

11. 應用

Generator 能夠暫停函數執行,返回任意表達式的值。這種特色使得 Generator 有多種應用場景。

(1)異步操做的同步化表達

Generator 函數的暫停執行的效果,意味着能夠把異步操做寫在yield表達式裏面,等到調用next方法時再日後執行。這實際上等同於不須要寫回調函數了,由於異步操做的後續操做能夠放在yield表達式下面,反正要等到調用next方法時再執行。因此,Generator 函數的一個重要實際意義就是用來處理異步操做,改寫回調函數。

function* loadUI() { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen(); } var loader = loadUI(); // 加載UI loader.next()  // 卸載UI loader.next() 

上面代碼中,第一次調用loadUI函數時,該函數不會執行,僅返回一個遍歷器。下一次對該遍歷器調用next方法,則會顯示Loading界面(showLoadingScreen),而且異步加載數據(loadUIDataAsynchronously)。等到數據加載完成,再一次使用next方法,則會隱藏Loading界面。能夠看到,這種寫法的好處是全部Loading界面的邏輯,都被封裝在一個函數,循序漸進很是清晰。

Ajax 是典型的異步操做,經過 Generator 函數部署 Ajax 操做,能夠用同步的方式表達。

function* main() { var result = yield request("http://some.url"); var resp = JSON.parse(result); console.log(resp.value); } function request(url) { makeAjaxCall(url, function(response){ it.next(response); }); } var it = main(); it.next(); 

上面代碼的main函數,就是經過 Ajax 操做獲取數據。能夠看到,除了多了一個yield,它幾乎與同步操做的寫法徹底同樣。注意,makeAjaxCall函數中的next方法,必須加上response參數,由於yield表達式,自己是沒有值的,老是等於undefined

下面是另外一個例子,經過 Generator 函數逐行讀取文本文件。

function* numbers() { let file = new FileReader("numbers.txt"); try { while(!file.eof) { yield parseInt(file.readLine(), 10); } } finally { file.close(); } } 

上面代碼打開文本文件,使用yield表達式能夠手動逐行讀取文件。

(2)控制流管理

若是有一個多步操做很是耗時,採用回調函數,可能會寫成下面這樣。

step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) {  // Do something with value4 }); }); }); }); 

採用 Promise 改寫上面的代碼。

Promise.resolve(step1) .then(step2) .then(step3) .then(step4) .then(function (value4) {  // Do something with value4 }, function (error) {  // Handle any error from step1 through step4 }) .done(); 

上面代碼已經把回調函數,改爲了直線執行的形式,可是加入了大量 Promise 的語法。Generator 函數能夠進一步改善代碼運行流程。

function* longRunningTask(value1) { try { var value2 = yield step1(value1); var value3 = yield step2(value2); var value4 = yield step3(value3); var value5 = yield step4(value4);  // Do something with value4 } catch (e) {  // Handle any error from step1 through step4 } } 

而後,使用一個函數,按次序自動執行全部步驟。

scheduler(longRunningTask(initialValue)); function scheduler(task) { var taskObj = task.next(task.value);  // 若是Generator函數未結束,就繼續調用 if (!taskObj.done) { task.value = taskObj.value scheduler(task); } } 

注意,上面這種作法,只適合同步操做,即全部的task都必須是同步的,不能有異步操做。由於這裏的代碼一獲得返回值,就繼續往下執行,沒有判斷異步操做什麼時候完成。若是要控制異步的操做流程,詳見後面的《異步操做》一章。

下面,利用for...of循環會自動依次執行yield命令的特性,提供一種更通常的控制流管理的方法。

let steps = [step1Func, step2Func, step3Func]; function* iterateSteps(steps){ for (var i=0; i< steps.length; i++){ var step = steps[i]; yield step(); } } 

上面代碼中,數組steps封裝了一個任務的多個步驟,Generator 函數iterateSteps則是依次爲這些步驟加上yield命令。

將任務分解成步驟以後,還能夠將項目分解成多個依次執行的任務。

let jobs = [job1, job2, job3]; function* iterateJobs(jobs){ for (var i=0; i< jobs.length; i++){ var job = jobs[i]; yield* iterateSteps(job.steps); } } 

上面代碼中,數組jobs封裝了一個項目的多個任務,Generator 函數iterateJobs則是依次爲這些任務加上yield*命令。

最後,就能夠用for...of循環一次性依次執行全部任務的全部步驟。

for (var step of iterateJobs(jobs)){ console.log(step.id); } 

再次提醒,上面的作法只能用於全部步驟都是同步操做的狀況,不能有異步操做的步驟。若是想要依次執行異步的步驟,必須使用後面的《異步操做》一章介紹的方法。

for...of的本質是一個while循環,因此上面的代碼實質上執行的是下面的邏輯。

var it = iterateJobs(jobs); var res = it.next(); while (!res.done){ var result = res.value;  // ... res = it.next(); } 

(3)部署 Iterator 接口

利用 Generator 函數,能夠在任意對象上部署 Iterator 接口。

function* iterEntries(obj) { let keys = Object.keys(obj); for (let i=0; i < keys.length; i++) { let key = keys[i]; yield [key, obj[key]]; } } let myObj = { foo: 3, bar: 7 }; for (let [key, value] of iterEntries(myObj)) { console.log(key, value); }  // foo 3 // bar 7 

上述代碼中,myObj是一個普通對象,經過iterEntries函數,就有了 Iterator 接口。也就是說,能夠在任意對象上部署next方法。

下面是一個對數組部署 Iterator 接口的例子,儘管數組原生具備這個接口。

function* makeSimpleGenerator(array){ var nextIndex = 0; while(nextIndex < array.length){ yield array[nextIndex++]; } } var gen = makeSimpleGenerator(['yo', 'ya']); gen.next().value // 'yo' gen.next().value // 'ya' gen.next().done  // true 

(4)做爲數據結構

Generator 能夠看做是數據結構,更確切地說,能夠看做是一個數組結構,由於 Generator 函數能夠返回一系列的值,這意味着它能夠對任意表達式,提供相似數組的接口。

function* doStuff() { yield fs.readFile.bind(null, 'hello.txt'); yield fs.readFile.bind(null, 'world.txt'); yield fs.readFile.bind(null, 'and-such.txt'); } 

上面代碼就是依次返回三個函數,可是因爲使用了 Generator 函數,致使能夠像處理數組那樣,處理這三個返回的函數。

for (task of doStuff()) {  // task是一個函數,能夠像回調函數那樣使用它 } 

實際上,若是用 ES5 表達,徹底能夠用數組模擬 Generator 的這種用法。

function doStuff() { return [ fs.readFile.bind(null, 'hello.txt'), fs.readFile.bind(null, 'world.txt'), fs.readFile.bind(null, 'and-such.txt') ]; } 

上面的函數,能夠用如出一轍的for...of循環處理!兩相一比較,就不難看出 Generator 使得數據或者操做,具有了相似數組的接口。

相關文章
相關標籤/搜索