<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                # Generator 函數 ## 簡介 ### 基本概念 Generator函數是ES6提供的一種異步編程解決方案,語法行為與傳統函數完全不同。本章詳細介紹Generator函數的語法和API,它的異步編程應用請看《異步操作》一章。 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*語句 如果在Generater函數內部,調用另一個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`? 下面是一個變通方法。首先,生成一個空對象,使用`bind`方法綁定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使得數據或者操作,具備了類似數組的接口。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看