“睡服”面试官系列第十八篇之generator函数的语法(建议收藏学习)
目錄
?
1簡介
1.1基本概念
1.2yield 表達式
1.3與 Iterator 接口的關系
2. next 方法的參數
3. for...of 循環
4. Generator.prototype.throw()
5. Generator.prototype.return()
6. next()、throw()、return() 的共同點
7. yield* 表達式
8. 作為對象屬性的 Generator 函數
9. Generator 函數的 this
10. 含義
10.1Generator 與狀態機
10.2Generator 與協程
11. 應用
11.1異步操作的同步化表達
11.2控制流管理
11.3部署 Iterator 接口
11.4作為數據結構
1簡介
1.1基本概念
Generator 函數是 ES6 提供的一種異步編程解決方案,語法行為與傳統函數完全不同。本章詳細介紹 Generator 函數的語法和 API,它的異步編程應用
Generator 函數有多種理解角度。從語法上,首先可以把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。
執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,還是一個遍歷器對象生成函數。返回的遍歷器對象,可以依次遍
歷 Generator 函數內部的每一個狀態。
形式上,Generator 函數是一個普通函數,但是有兩個特征。一是, function 關鍵字與函數名之間有一個星號;二是,函數體內部使用 yield 表達式,
定義不同的內部狀態( yield 在英語里的意思就是“產出”)
上面代碼定義了一個 Generator 函數 helloWorldGenerator ,它內部有兩個 yield 表達式( hello 和 world ),即該函數有三個狀態:hello,world 和
return 語句(結束執行)。
然后,Generator 函數的調用方法與普通函數一樣,也是在函數名后面加上一對圓括號。不同的是,調用 Generator 函數后,該函數并不執行,返回的
也不是函數運行結果,而是一個指向內部狀態的指針對象,也就是上一章介紹的遍歷器對象(Iterator Object)。
下一步,必須調用遍歷器對象的 next 方法,使得指針移向下一個狀態。也就是說,每次調用 next 方法,內部指針就從函數頭部或上一次停下來的地方開
始執行,直到遇到下一個 yield 表達式(或 return 語句)為止。換言之,Generator 函數是分段執行的, yield 表達式是暫停執行的標記,而 next 方法
可以恢復執行。
上面代碼一共調用了四次 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 關鍵字與函數名之間的星號,寫在哪個位置。這導致下面的寫法都能通過。
1.2yield 表達式
由于 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)的語法功能
上面代碼中, yield 后面的表達式 123 + 456 ,不會立即求值,只會在 next 方法將指針移到這一句時,才會求值。
yield 表達式與 return 語句既有相似之處,也有區別。相似之處在于,都能返回緊跟在語句后面的那個表達式的值。區別在于每次遇到 yield ,函數暫停
執行,下一次再從該位置繼續向后執行,而 return 語句不具備位置記憶的功能。一個函數里面,只能執行一次(或者說一個) return 語句,但是可以執
行多次(或者說多個) yield 表達式。正常函數只能返回一個值,因為只能執行一次 return ;Generator 函數可以返回一系列的值,因為可以有任意多
個 yield 。從另一個角度看,也可以說 Generator 生成了一系列的值,這也就是它的名稱的來歷(英語中,generator 這個詞是“生成器”的意思)。
Generator 函數可以不用 yield 表達式,這時就變成了一個單純的暫緩執行函數。
上面代碼中,函數 f 如果是普通函數,在為變量 generator 賦值時就會執行。但是,函數 f 是一個 Generator 函數,就變成只有調用 next 方法時,函數
f 才會執行。
另外需要注意, yield 表達式只能用在 Generator 函數里面,用在其他地方都會報錯。
上面代碼在一個普通函數中使用 yield 表達式,結果產生一個句法錯誤。
下面是另外一個例子
上面代碼也會產生句法錯誤,因為 forEach 方法的參數是一個普通函數,但是在里面使用了 yield 表達式(這個函數里面還使用了 yield* 表達式,詳細介
紹見后文)。一種修改方法是改用 for 循環
另外, 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 }1.3與 Iterator 接口的關系
任意一個對象的 Symbol.iterator 方法,等于該對象的遍歷器生成函數,調用該函數會返回該對象的一個遍歷器對象。
由于 Generator 函數就是遍歷器生成函數,因此可以把 Generator 賦值給對象的 Symbol.iterator 屬性,從而使得該對象具有 Iterator 接口。
上面代碼中,Generator 函數賦值給 Symbol.iterator 屬性,從而使得 myIterable 對象具有了 Iterator 接口,可以被 ... 運算符遍歷了。
Generator 函數執行后,返回一個遍歷器對象。該對象本身也具有 Symbol.iterator 屬性,執行后返回自身
上面代碼中, gen 是一個 Generator 函數,調用它會生成一個遍歷器對象 g 。它的 Symbol.iterator 屬性,也是一個遍歷器對象生成函數,執行后返回它
自己。
2. 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 函數運行的不同階段,從外部向內部注入不同的值,從而調整
函數行為。
再看一個例子
上面代碼中,第二次運行 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 函數內部輸入值的例子
上面代碼是一個很直觀的例子,每次通過 next 方法向 Generator 函數輸入值,然后打印出來。
如果想要第一次調用 next 方法時,就能夠輸入值,可以在 Generator 函數外面再包一層。
上面代碼中,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 屬性為 true , for...of 循環就會中
止,且不包含該返回對象,所以上面代碼的 return 語句返回的 6 ,不包括在 for...of 循環之中。
下面是一個利用 Generator 函數和 for...of 循環,實現斐波那契數列的例子
從上面代碼可見,使用 for...of 語句時不需要使用 next 方法。
利用 for...of 循環,可以寫出遍歷任意對象(object)的方法。原生的 JavaScript 對象沒有遍歷接口,無法使用 for...of 循環,通過 Generator 函數
為它加上這個接口,就可以用了
上面代碼中,對象 jane 原生不具備 Iterator 接口,無法用 for...of 遍歷。這時,我們通過 Generator 函數 objectEntries 為它加上遍歷器接口,就可
以用 for...of 遍歷了。加上遍歷器接口的另一種寫法是,將 Generator 函數加到對象的 Symbol.iterator 屬性上面。
除了 for...of 循環以外,擴展運算符( ... )、解構賦值和 Array.from 方法內部調用的,都是遍歷器接口。這意味著,它們都可以將 Generator 函數
返回的 Iterator 對象,作為參數。
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 對象的實例。
注意,不要混淆遍歷器對象的 throw 方法和全局的 throw 命令。上面代碼的錯誤,是用遍歷器對象的 throw 方法拋出的,而不是用 throw 命令拋出的。后
者只能被函數體外的 catch 語句捕獲。
上面代碼之所以只捕獲了 a ,是因為函數體外的 catch 語句塊,捕獲了拋出的 a 錯誤以后,就不會再繼續 try 代碼塊里面剩余的語句了。
如果 Generator 函數內部沒有部署 try...catch 代碼塊,那么 throw 方法拋出的錯誤,將被外部 try...catch 代碼塊捕獲。
上面代碼中,Generator 函數 g 內部沒有部署 try...catch 代碼塊,所以拋出的錯誤直接被外部 catch 代碼塊捕獲。
如果 Generator 函數內部和外部,都沒有部署 try...catch 代碼塊,那么程序將報錯,直接中斷執行
上面代碼中, g.throw 拋出錯誤以后,沒有任何 try...catch 代碼塊可以捕獲這個錯誤,導致程序報錯,中斷執行。
throw 方法被捕獲以后,會附帶執行下一條 yield 表達式。也就是說,會附帶執行一次 next 方法
上面代碼中, g.throw 方法被捕獲以后,自動執行了一次 next 方法,所以會打印 b 。另外,也可以看到,只要 Generator 函數內部部署了 try...catch
代碼塊,那么遍歷器的 throw 方法拋出的錯誤,不影響下一次遍歷。
另外, throw 命令與 g.throw 方法是無關的,兩者互不影響。
上面代碼中, throw 命令拋出的錯誤不會影響到遍歷器的狀態,所以兩次執行 next 方法,都進行了正確的操作。
這種函數體內捕獲錯誤的機制,大大方便了對錯誤的處理。多個 yield 表達式,可以只用一個 try...catch 代碼塊來捕獲錯誤。如果使用回調函數的寫
法,想要捕獲多個錯誤,就不得不為每個函數內部寫一個錯誤處理語句,現在只在 Generator 函數內部寫一次 catch 語句就可以了。
Generator 函數體外拋出的錯誤,可以在函數體內捕獲;反過來,Generator 函數體內拋出的錯誤,也可以被函數體外的 catch 捕獲
上面代碼中,第二個 next 方法向函數體內傳入一個參數 42,數值是沒有 toUpperCase 方法的,所以會拋出一個 TypeError 錯誤,被函數體外的 catch
捕獲。
一旦 Generator 執行過程中拋出錯誤,且沒有被內部捕獲,就不會再執行下去了。如果此后還調用 next 方法,將返回一個 value 屬性等于 undefined 、
done 屬性等于 true 的對象,即 JavaScript 引擎認為這個 Generator 已經運行結束了。
上面代碼一共三次運行 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 。
如果 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() // { 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 表達式替換成一個值。
上面代碼中,第二個 next(1) 方法就相當于將 yield 表達式替換成一個值 1 。如果 next 方法沒有參數,就相當于替換成 undefined 。
throw() 是將 yield 表達式替換成一個 throw 語句。
7. yield* 表達式
如果在 Generator 函數內部,調用另一個 Generator 函數,默認情況下是沒有效果的。
function* foo() { yield 'a'; yield 'b'; } function* bar() { yield 'x'; foo(); yield 'y'; } for (let v of bar()){ console.log(v); } // "x" // "y"上面代碼中, foo 和 bar 都是 Generator 函數,在 bar 里面調用 foo ,是不會有效果的。
這個就需要用到 yield* 表達式,用來在一個 Generator 函數里面執行另一個 Generator 函數。
再來看一個對比的例子。
function* 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* 表
達式
上面代碼中, delegatingIterator 是代理者, delegatedIterator 是被代理者。由于 yield* delegatedIterator 語句得到的值,是一個遍歷器,所以要
用星號表示。運行結果就是使用一個遍歷器,遍歷了多個 Generator 函數,有遞歸的效果。
yield* 后面的 Generator 函數(沒有 return 語句時),等同于在 Generator 函數內部,部署一個 for...of 循環。
上面代碼說明, 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* 遍歷。
上面代碼中, yield 表達式返回整個字符串, yield* 語句返回單個字符。因為字符串具有 Iterator 接口,所以被 yield* 遍歷。
如果被代理的 Generator 函數有 return 語句,那么就可以向代理它的 Generator 函數返回數據。
上面代碼在第四次調用 next 方法的時候,屏幕上會有輸出,這是因為函數 foo 的 return 語句,向函數 bar 提供了返回值。
再看一個例子
上面代碼中,存在兩次遍歷。第一次是擴展運算符遍歷函數 logReturned 返回的遍歷器對象,第二次是 yield* 語句遍歷函數 genFuncWithReturn 返回的遍
歷器對象。這兩次遍歷的效果是疊加的,最終表現為擴展運算符遍歷函數 genFuncWithReturn 返回的遍歷器對象。所以,最后的數據表達式得到的值等于
[ 'a', 'b' ] 。但是,函數 genFuncWithReturn 的 return 語句的返回值 The result ,會返回給函數 logReturned 內部的 result 變量,因此會有終端輸
出。
yield* 命令可以很方便地取出嵌套數組的所有成員
下面是一個稍微復雜的例子,使用 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 函數。
它的完整形式如下,與上面的寫法是等價的
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 對象。
上面代碼中,Generator 函數 g 在 this 對象上面添加了一個屬性 a ,但是 obj 對象拿不到這個屬性。
Generator 函數也不能跟 new 命令一起用,會報錯。
上面代碼中, new 命令跟構造函數 F 一起使用,結果報錯,因為 F 不是構造函數。
那么,有沒有辦法讓 Generator 函數返回一個正常的對象實例,既可以用 next 方法,又可以獲得正常的 this ?
下面是一個變通方法。首先,生成一個空對象,使用 call 方法綁定 Generator 函數內部的 this 。這樣,構造函數調用以后,這個空對象就是
Generator 函數的實例對象了
上面代碼中,首先是 F 內部的 this 對象綁定 obj 對象,然后調用它,返回一個 Iterator 對象。這個對象執行三次 next 方法(因為 F 內部有兩個 yield 表
達式),完成 F 內部所有代碼的運行。這時,所有內部屬性都綁定在 obj 對象上了,因此 obj 對象也就成了 F 的實例。
上面代碼中,執行的是遍歷器對象 f ,但是生成的對象實例是 obj ,有沒有辦法將這兩個對象統一呢?
一個辦法就是將 obj 換成 F.prototype
再將 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 // 310. 含義
10.1Generator 與狀態機
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 之所以可以不用外部變量保存狀態,是因為它本身就包含了一個狀態信息,即目前是否處于暫停態
10.2Generator 與協程
協程(coroutine)是一種程序運行的方式,可以理解成“協作的線程”或“協作的函數”。協程既可以用單線程實現,也可以用多線程實現。前者是一種特殊
的子例程,后者是一種特殊的線程
(1)協程與子例程的差異
傳統的“子例程”(subroutine)采用堆棧式“后進先出”的執行方式,只有當調用的子函數完全執行完畢,才會結束執行父函數。協程與其不同,多個線程
(單線程情況下,即多個函數)可以并行執行,但是只有一個線程(或函數)處于正在運行的狀態,其他線程(或函數)都處于暫停態(suspended),
線程(或函數)之間可以交換執行權。也就是說,一個線程(或函數)執行到一半,可以暫停執行,將執行權交給另一個線程(或函數),等到稍后收回
執行權的時候,再恢復執行。這種可以并行執行、交換執行權的線程(或函數),就稱為協程。
從實現上看,在內存中,子例程只使用一個棧(stack),而協程是同時存在多個棧,但只有一個棧是在運行狀態,也就是說,協程是以多占用內存為代
價,實現多任務的并行。
(2)協程與普通線程的差異
不難看出,協程適合用于多任務運行的環境。在這個意義上,它與普通的線程很相似,都有自己的執行上下文、可以分享全局變量。它們的不同之處在
于,同一時間可以有多個線程處于運行狀態,但是運行的協程只能有一個,其他協程都處于暫停狀態。此外,普通的線程是搶先式的,到底哪個線程優先
得到資源,必須由運行環境決定,但是協程是合作式的,執行權由協程自己分配。
由于 JavaScript 是單線程語言,只能保持一個調用棧。引入協程以后,每個任務可以保持自己的調用棧。這樣做的最大好處,就是拋出錯誤的時候,可以
找到原始的調用棧。不至于像異步操作的回調函數那樣,一旦出錯,原始的調用棧早就結束。
Generator 函數是 ES6 對協程的實現,但屬于不完全實現。Generator 函數被稱為“半協程”(semi-coroutine),意思是只有 Generator 函數的調用
者,才能將程序的執行權還給 Generator 函數。如果是完全執行的協程,任何函數都可以讓暫停的協程繼續執行。
如果將 Generator 函數當作協程,完全可以將多個需要互相協作的任務寫成 Generator 函數,它們之間使用 yield 表示式交換控制權。
11. 應用
Generator 可以暫停函數執行,返回任意表達式的值。這種特點使得 Generator 有多種應用場景
11.1異步操作的同步化表達
Generator 函數的暫停執行的效果,意味著可以把異步操作寫在 yield 表達式里面,等到調用 next 方法時再往后執行。這實際上等同于不需要寫回調函
數了,因為異步操作的后續操作可以放在 yield 表達式下面,反正要等到調用 next 方法時再執行。所以,Generator 函數的一個重要實際意義就是用來
處理異步操作,改寫回調函數。
上面代碼中,第一次調用 loadUI 函數時,該函數不會執行,僅返回一個遍歷器。下一次對該遍歷器調用 next 方法,則會顯示 Loading 界面
( showLoadingScreen ),并且異步加載數據( loadUIDataAsynchronously )。等到數據加載完成,再一次使用 next 方法,則會隱藏 Loading 界面。可
以看到,這種寫法的好處是所有 Loading 界面的邏輯,都被封裝在一個函數,按部就班非常清晰。
Ajax 是典型的異步操作,通過 Generator 函數部署 Ajax 操作,可以用同步的方式表達
上面代碼的 main 函數,就是通過 Ajax 操作獲取數據。可以看到,除了多了一個 yield ,它幾乎與同步操作的寫法完全一樣。注意, makeAjaxCall 函數
中的 next 方法,必須加上 response 參數,因為 yield 表達式,本身是沒有值的,總是等于 undefined 。
下面是另一個例子,通過 Generator 函數逐行讀取文本文件
上面代碼打開文本文件,使用 yield 表達式可以手動逐行讀取文件。
11.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 命令的特性,提供一種更一般的控制流管理的方法
上面代碼中,數組 steps 封裝了一個任務的多個步驟,Generator 函數 iterateSteps 則是依次為這些步驟加上 yield 命令。
將任務分解成步驟之后,還可以將項目分解成多個依次執行的任務
上面代碼中,數組 jobs 封裝了一個項目的多個任務,Generator 函數 iterateJobs 則是依次為這些任務加上 yield* 命令。
最后,就可以用 for...of 循環一次性依次執行所有任務的所有步驟。
再次提醒,上面的做法只能用于所有步驟都是同步操作的情況,不能有異步操作的步驟。
章介紹的方法。
for...of 的本質是一個 while 循環,所以上面的代碼實質上執行的是下面的邏輯
11.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 接口的例子,盡管數組原生具有這個接口。
11.4作為數據結構
Generator 可以看作是數據結構,更確切地說,可以看作是一個數組結構,因為 Generator 函數可以返回一系列的值,這意味著它可以對任意表達式,
提供類似數組的接口。
上面代碼就是依次返回三個函數,但是由于使用了 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 使得數據或者操作,具備了類似數組的接口。
總結
本博客源于本人閱讀相關書籍和視頻總結,創作不易,謝謝點贊支持。學到就是賺到。我是歌謠,勵志成為一名優秀的技術革新人員。
歡迎私信交流,一起學習,一起成長。
推薦鏈接 其他文件目錄參照
“睡服“面試官系列之各系列目錄匯總(建議學習收藏)
總結
以上是生活随笔為你收集整理的“睡服”面试官系列第十八篇之generator函数的语法(建议收藏学习)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: QT播放器布局
- 下一篇: 博士德服务器帐套维护密码忘记,T+账套主