【第829期】你不懂JS:ES6與將來 組織(上)

【第829期】你不懂JS:ES6與將來 組織(上)

圖片

前言html

《你不懂js》系列又來了,很久不露面了,早讀君在看的時候內心在想,這種你們運用度如何,平時工做會用到嗎?文章有點長,你能夠大概的瞭解下,今天早讀文章來自前端早讀課專欄做者@HetfieldJoe的分享。前端


正文從這開始~git


編寫JS代碼是一回事兒,而合理地組織它是另外一回事兒。利用常見的組織和重用模式在很大程度上改善了你代碼的可讀性和可理解性。記住:代碼在與其餘開發者交流上起的做用,與在給計算機喂指令上起的做用一樣重要。es6


ES6擁有幾種重要的特性能夠顯著改善這些模式,包括:迭代器,generator,模塊,和類。github


迭代器算法


迭代器(iterator) 是一種結構化的模式,用於從一個信息源中以一次一個的方式抽取信息。這種模式在程序設計中存在好久了。並且不能否認的是,不知從何時起JS開發者們就已經特別地設計並實現了迭代器,因此它根本不是什麼新的話題。數據庫


ES6所作的是,爲迭代器引入了一個隱含的標準化接口。許多在JavaScript中內建的數據結構如今都會暴露一個實現了這個標準的迭代器。並且你也能夠構建本身的遵循一樣標準的迭代器,來使互用性最大化。數組


迭代器是一種消費數據的方法,它是組織有順序的,相繼的,基於抽取的。緩存


舉個例子,你可能實現一個工具,它在每次被請求時產生一個新的惟一的標識符。或者你可能循環一個固定的列表以輪流的方式產生一系列無限多的值。或者你能夠在一個數據庫查詢的結果上添加一個迭代器來一次抽取一行結果。網絡


雖然在JS中它們不常常以這樣的方式被使用,可是迭代器還能夠認爲是每次控制行爲中的一個步驟。這會在考慮generator時獲得至關清楚的展現(參見本章稍後的「Generator」),雖然你固然能夠不使用generator而作一樣的事。


接口


在本書寫做的時候,ES6的25.1.1.2部分 (https://people.mozilla.org/~jorendorff/es6-draft.html#sec-iterator-interface) 詳述了Iterator接口,它有以下的要求:

image.png


有兩個可選成員,有些迭代器用它們進行了擴展:

image.png


接口IteratorResult被規定爲:

image.png


注意: 我稱這些接口是隱含的,不是由於它們沒有在語言規範中被明確地被說出來 —— 它們被說出來了!—— 而是由於它們沒有做爲能夠直接訪問的對象暴露給代碼。在ES6中,JavaScript不支持任何「接口」的概念,因此在你本身的代碼中遵循它們純粹是慣例上的。可是,不論JS在何處須要一個迭代器 —— 例如在一個for..of循環中 —— 你提供的東西必須遵循這些接口,不然代碼就會失敗。


還有一個Iterable接口,它描述了必定可以產生迭代器的對象:

image.png


若是你回憶一下第二章的「內建Symbol」,@@iterator是一種特殊的內建symbol,表示能夠爲對象產生迭代器的方法。


IteratorResult


IteratorResult接口規定從任何迭代器操做的返回值都是這樣形式的對象:

image.png


內建迭代器將老是返回這種形式的值,固然,更多的屬性也容許出如今這個返回值中,若是有必要的話。


例如,一個自定義的迭代器可能會在結果對象中加入額外的元數據(好比,數據是從哪裏來的,取得它花了多久,緩存過時的時間長度,下次請求的恰當頻率,等等)。


注意: 從技術上講,在值爲undefined的狀況下,value是可選的,它將會被認爲是不存在或者是沒有被設置。由於無論它是表示的就是這個值仍是徹底不存在,訪問res.value都將會產生undefined,因此這個屬性的存在/不存在更大程度上是一個實現或者優化(或二者)的細節,而非一個功能上的問題。


next()迭代


讓咱們來看一個數組,它是一個可迭代對象,能夠生成一個迭代器來消費它的值:

image.png


每一次定位在Symbol.iterator上的方法在值arr上被調用時,它都將生成一個全新的迭代器。大多數的數據結構都會這麼作,包括全部內建在JS中的數據結構。


然而,像事件隊列這樣的結構也許只能生成一個單獨的迭代器(單例模式)。或者某種結構可能在同一時間內只容許存在一個惟一的迭代器,要求當前的迭代器必須完成,才能建立一個新的。


前一個代碼段中的it迭代器不會再你獲得值3時報告done: true。你必須再次調用next(),實質上越過數組末尾的值,才能獲得完成信號done: true。在這一節稍後會清楚地講解這種設計方式的緣由,可是它一般被認爲是一種最佳實踐。


基本類型的字符串值也默認地是可迭代對象:

image.png


注意: 從技術上講,這個基本類型值自己不是可迭代對象,但多虧了「封箱」,"hello world"被強制轉換爲它的String對象包裝形式,它 纔是一個可迭代對象。更多信息參見本系列的 類型與文法。


ES6還包括幾種新的數據結構,稱爲集合(參見第五章)。這些集合不只自己就是可迭代對象,並且它們還提供API方法來生成一個迭代器,例如:

image.png


一個迭代器的next(..)方法可以可選地接受一個或多個參數。大多數內建的迭代器不會實施這種能力,雖然一個generator的迭代器絕對會這麼作(參見本章稍後的「Generator」)。


根據通常的慣例,包括全部的內建迭代器,在一個已經被耗盡的迭代器上調用next(..)不是一個錯誤,而是簡單地持續返回結果{ value: undefined, done: true }。


可選的return(..)和throw(..)


在迭代器接口上的可選方法 —— return(..)和throw(..) —— 在大多數內建的迭代器上都沒有被實現。可是,它們在generator的上下文環境中絕對有某些含義,因此更具體的信息能夠參看「Generator」。


return(..)被定義爲向一個迭代器發送一個信號,告知它消費者代碼已經完成並且不會再從它那裏抽取更多的值。這個信號能夠用於通知生產者(應答next(..)調用的迭代器)去實施一些可能的清理做業,好比釋放/關閉網絡,數據庫,或者文件引用資源。


若是一個迭代器擁有return(..),並且發生了能夠自動被解釋爲非正常或者提早終止消費迭代器的任何狀況,return(..)就將會被自動調用。你也能夠手動調用return(..)。


return(..)將會像next(..)同樣返回一個IteratorResult對象。通常來講,你向return(..)發送的可選值將會在這個IteratorResult中做爲value發送回來,雖然在一些微妙的狀況下這可能不成立。


throw(..)被用於向一個迭代器發送一個異常/錯誤信號,與return(..)隱含的完成信號相比,它可能會被迭代器用於不一樣的目的。它不必定像return(..)同樣暗示着迭代器的徹底中止。


例如,在generator迭代器中,throw(..)實際上會將一個被拋出的異常注射到generator暫停的執行環境中,這個異常能夠用try..catch捕獲。一個未捕獲的throw(..)異常將會致使generator的迭代器異常停止。


注意: 根據通常的慣例,在return(..)或throw(..)被調用以後,一個迭代器就不該該在產生任何結果了。


迭代器循環


正如咱們在第二章的「for..of」一節中講解的,ES6的for..of循環能夠直接消費一個規範的可迭代對象。


若是一個迭代器也是一個可迭代對象,那麼它就能夠直接與for..of循環一塊兒使用。經過給予迭代器一個簡單地返回它自身的Symbol.iterator方法,你就可使它成爲一個可迭代對象:

image.png


如今咱們就能夠用一個for..of循環來消費迭代器it了:

image.png


爲了徹底理解這樣的循環如何工做,回憶下第二章中的for..of循環的for等價物:

image.png


若是你仔細觀察,你會發現it.next()是在每次迭代以前被調用的,而後res.done才被查詢。若是res.done是true,那麼這個表達式將會求值爲false因而此次迭代不會發生。


回憶一下以前咱們建議說,迭代器通常不該與最終預期的值一塊兒返回done: true。如今你知道爲何了。


若是一個迭代器返回了{ done: true, value: 42 },for..of循環將徹底扔掉值42。所以,假定你的迭代器可能會被for..of循環或它的for等價物這樣的模式消費的話,你可能應當等到你已經返回了全部相關的迭代值以後才返回done: true來表示完成。


警告: 固然,你能夠有意地將你的迭代器設計爲將某些相關的value與done: true同時返回。但除非你將此狀況在文檔中記錄下來,不然不要這麼作,由於這樣會隱含地強制你的迭代器消費者使用一種,與咱們剛纔描述的for..of或它的手動等價物不一樣的模式來進行迭代。


自定義迭代器


除了標準的內建迭代器,你還能夠製造你本身的迭代器!全部使它們能夠與ES6消費設施(例如,for..of循環和...操做符)進行互動的代價就是遵循恰當的接口。


讓咱們試着構建一個迭代器,它可以以斐波那契(Fibonacci)數列的形式產生無限多的數字序列:

image.png


警告: 若是咱們沒有插入break條件,這個for..of循環將會永遠運行下去,這回破壞你的程序,所以可能不是咱們想要的!


方法Fib[Symbol.iterator]()在被調用時返回帶有next()和return(..)方法的迭代器對象。它的狀態經過變量n1和n2維護在閉包中。


接下來讓咱們考慮一個迭代器,它被設計爲執行一系列(也叫隊列)動做,一次一個:

image.png


在tasks上的迭代器步過在數組屬性actions中找到的函數,並每次執行它們中的一個,並傳入你傳遞給next(..)的任何參數值,並在標準的IteratorResult對象中向你返回任何它返回的東西。


這是咱們如何使用這個tasks隊列:

image.png


這種特別的用法證明了迭代器能夠是一種具備組織功能的模式,不只僅是數據。這也聯繫着咱們在下一節關於generator將要看到的東西。


你甚至能夠更有創意一些,在一塊數據上定義一個表示元操做的迭代器。例如,咱們能夠爲默認從0開始遞增至(或遞減至,對於負數來講)指定數字的一組數字定義一個迭代器。


考慮以下代碼:

image.png


如今,這種創意給了咱們什麼技巧?

image.png


這是一些有趣的技巧,雖然其實際用途有些值得商榷。可是再一次,有人可能想知道爲何ES6沒有提供如此微小但討喜的特性呢?


若是我連這樣的提醒都沒給過你,那就是個人疏忽:像我在前面的代碼段中作的那樣擴展原生原型,是一件你須要當心並瞭解潛在的危害後才應該作的事情。


在這樣的狀況下,你與其餘代碼或者將來的JS特性發生衝突的可能性很是低。可是要當心微小的可能性。並在文檔中爲後人詳細記錄下你在作什麼。


消費迭代器


咱們已經看到了使用for..of循環來一個元素一個元素地消費一個迭代器。可是還有一些其餘的ES6結構能夠消費迭代器。


讓咱們考慮一下附着這個數組上的迭代器(雖然任何咱們選擇的迭代器都將擁有以下的行爲):

image.png


擴散操做符...將徹底耗盡一個迭代器。考慮以下代碼:

image.png


...還能夠在一個數組內部擴散一個迭代器:

image.png


數組解構(參見第二章的「解構」)能夠部分地或者徹底地(若是與一個...剩餘/收集操做符一塊兒使用)消費一個迭代器:

image.png


Generator


全部的函數都會運行至完成,對吧?換句話說,一旦一個函數開始運行,在它完成以前沒有任何東西可以打斷它。


至少對於到目前爲止的JavaScript的整個歷史來講是這樣的。在ES6中,引入了一個有些異乎尋常的新形式的函數,稱爲generator。一個generator能夠在運行期間暫停它本身,還能夠當即或者稍後繼續運行。因此顯然它沒有普通函數那樣的運行至完成的保證。


另外,在運行期間的每次暫停/繼續輪迴都是一個雙向消息傳遞的好機會,generator能夠在這裏返回一個值,而使它繼續的控制端代碼能夠發回一個值。


就像前一節中的迭代器同樣,有種方式能夠考慮generator是什麼,或者說它對什麼最有用。對此沒有一個正確的答案,但咱們將試着從幾個角度考慮。


語法


generator函數使用這種新語法聲明:

image.png


*的位置在功能上可有可無。一樣的聲明還能夠寫作如下的任意一種:

image.png


這裏 惟一 的區別就是風格的偏好。大多數其餘的文獻彷佛喜歡function* foo(..) { .. }。我喜歡function *foo(..) { .. },因此這就是我將在本書剩餘部分中表示它們的方法。


我這樣作的理由實質上純粹是爲了教學。在這本書中,當我引用一個generator函數時,我將使用*foo(..),與普通函數的foo(..)相對。我發現*foo(..)與function *foo(..) { .. }中*的位置更加吻合。


另外,就像咱們在第二章的簡約方法中看到的,在對象字面量中有一種簡約generator形式:

image.png


我要說在簡約generator中,*foo() { .. }要比* foo() { .. }更天然。這進一步代表了爲什麼使用*foo()匹配一致性。


一致性使理解與學習更輕鬆。


執行一個Generator


雖然一個generator使用*進行聲明,可是你依然能夠像一個普通函數那樣執行它:

image.png


你依然能夠傳給它參數值,就像:

image.png


主要區別在於,執行一個generator,好比foo(5,10),並不實際運行generator中的代碼。取而代之的是,它生成一個迭代器來控制generator執行它的代碼。


咱們將在稍後的「迭代器控制」中回到這個話題,可是簡要地說:

image.png


yield


Generator還有一個你能夠在它們內部使用的新關鍵字,用來表示暫停點:yield。考慮以下代碼:

image.png


在這個*foo()generator中,前兩行的操做將會在開始時運行,而後yield將會暫停這個generator。若是這個generator被繼續,*foo()的最後一行將運行。在一個generator中yield能夠出現任意屢次(或者,在技術上講,根本不出現!)。


你甚至能夠在一個循環內部放置yield,它能夠表示一個重複的暫停點。事實上,一個永不完成的循環就意味着一個永不完成的generator,這是徹底合法的,並且有時候徹底是你須要的。


yield不僅是一個暫停點。它是在暫停generator時發送出一個值的表達式。這裏是一個位於generator中的while..true循環,它每次迭代時yield出一個新的隨機數:

image.png


yield ..表達式不只發送一個值 —— 不帶值的yield與yield undefined相同 —— 它還接收(也就是,被替換爲)最終的繼續值。考慮以下代碼:

image.png


這個generator在暫停它本身時將首先yield出值10。當你繼續這個generator時 —— 使用咱們先前提到的it.next(..) —— 不管你使用什麼值繼續它,這個值都將替換/完成整個表達式yield 10,這意味着這個值將被賦值給變量x


一個yield..表達式能夠出如今任意普通表達式可能出現的地方。例如:



這裏的*foo()有四個yield ..表達式。其中每一個yield都會致使generator暫停以等待一個繼續值,這個繼續值稍後被用於各個表達式環境中。


yield在技術上講不是一個操做符,雖然像yield 1這樣使用時看起來確實很像。由於yield能夠像var x = yield這樣徹底經過本身被使用,因此將它認爲是一個操做符有時使人困惑。


從技術上講,yield ..與a = 3這樣的賦值表達式擁有相同的「表達式優先級」 —— 概念上和操做符優先級很類似。這意味着yield ..基本上能夠出如今任何a = 3能夠合法出現的地方。


讓咱們展現一下這種對稱性:

image.png


注意: 若是你好好考慮一下,認爲一個yield ..表達式與一個賦值表達式的行爲類似在概念上有些道理。當一個被暫停的generator被繼續時,它就以一種與被這個繼續值「賦值」區別不大的方式,被這個值完成/替換。


要點:若是你須要yield ..出如今a = 3這樣的賦值本不被容許出現的位置,那麼它就須要被包在一個( )中。


由於yield關鍵字的優先級很低,幾乎任何出如今yield ..以後的表達式都會在被yield發送以前首先被計算。只有擴散操做符...和逗號操做符,擁有更低的優先級,這意味着他們會在yield已經被求值以後纔會被處理。


因此正如帶有多個操做符的普通語句同樣,存在另外一個可能須要( )來覆蓋(提高)yield的低優先級的狀況,就像這些表達式之間的區別:

image.png


和=賦值同樣,yield也是「右結合性」的,這意味着多個接連出現的yield表達式被視爲從右到左被( .. )分組。因此,yield yield yield 3將被視爲yield (yield (yield 3))。像((yield) yield) yield 3這樣的「左結合性」解釋沒有意義。


和其餘操做符同樣,yield與其餘操做符或yield組合時爲了使你的意圖沒有歧義,使用( .. )分組是一個好主意,即便這不是嚴格要求的。


注意: 更多關於操做符優先級和結合性的信息,參見本系列的 類型與文法。


yield *


與*使一個function聲明成爲一個function *generator聲明的方式同樣,一個*使yield成爲一個機制很是不一樣的yield *,稱爲 yield委託。從文法上講,yield *..的行爲與yield ..相同,就像在前一節討論過的那樣。


yield * ..須要一個可迭代對象;而後它調用這個可迭代對象的迭代器,並將它本身的宿主generator的控制權委託給那個迭代器,直到它被耗盡。考慮以下代碼:

image.png


注意: 與generator聲明中*的位置(早先討論過)同樣,在yield *表達式中的*的位置在風格上由你來決定。大多數其餘文獻偏好yield* ..,可是我喜歡yield *..,理由和咱們已經討論過的相同。


值[1,2,3]產生一個將會步過它的值的迭代器,因此generator*foo()將會在被消費時產生這些值。另外一種說明這種行爲的方式是,yield委託到了另外一個generator:

image.png


當*bar()調用*foo()產生的迭代器經過yield *受到委託,意味着不管*foo()產生什麼值都會被*bar()產生。


在yield ..中表達式的完成值來自於使用it.next(..)繼續generator,而yield *..表達式的完成值來自於受到委託的迭代器的返回值(若是有的話)。


內建的迭代器通常沒有返回值,正如咱們在本章早先的「迭代器循環」一節的末尾講過的。可是若是你定義你本身的迭代器(或者generator),你就能夠將它設計爲return一個值,yield *..將會捕獲它:

image.png


雖然值1,2,和3從*foo()中被yield出來,而後從*bar()中被yield出來,可是從*foo()中返回的值4是表達式yield *foo()的完成值,而後它被賦值給x。


由於yield *能夠調用另外一個generator(經過委託到它的迭代器的方式),它還能夠經過調用本身來實施某種generator遞歸:

image.png


取得foo(1)的結果並調用迭代器的next()來使它運行它的遞歸步驟,結果將是24。第一次*foo()運行時x擁有值1,它是x < 3。x + 1被遞歸地傳遞到*foo(..),因此以後的x是2。再一次遞歸調用致使x爲3。


如今,由於x < 3失敗了,遞歸中止,並且return 3 * 2將6給回前一個調用的yeild *..表達式,它被賦值給x。另外一個return 6 * 2返回12給前一個調用的x。最終12 * 2,即24,從generator*foo(..)運行的完成中被返回。


迭代器控制


早先,咱們簡要地介紹了generator是由迭代器控制的概念。如今讓咱們完整地深刻這個話題。


回憶一下前一節的遞歸*for(..)。這是咱們如何運行它:

圖片


在這種狀況下,generator並無真正暫停過,由於這裏沒有yield ..表達式。而yield *只是經過遞歸調用保持當前的迭代步驟繼續運行下去。因此,僅僅對迭代器的next()函數進行一次調用就徹底地運行了generator。


如今讓咱們考慮一個有多個步驟而且所以有多個產生值的generator:

image.png


咱們已經知道咱們能夠是使用一個for..of循環來消費一個迭代器,即使它是一個附着在*foo()這樣的generator上:

image.png


注意: for..of循環須要一個可迭代對象。一個generator函數引用(好比foo)自己不是一個可迭代對象;你必須使用foo()來執行它以獲得迭代器(它也是一個可迭代對象,正如咱們在本章早先講解過的)。理論上你可使用一個實質上僅僅執行return this()的Symbol.iterator函數來擴展GeneratorPrototype(全部generator函數的原型)。這將使foo引用自己成爲一個可迭代對象,也就意味着for (var v of foo) { .. }(注意在foo上沒有())將能夠工做。


讓咱們手動迭代這個generator:

image.png


若是你仔細觀察,這裏有三個yield語句和四個next()調用。這可能看起來像是一個奇怪的不匹配。事實上,假定全部的東西都被求值而且generator徹底運行至完成的話,next()調用將老是比yield表達式多一個。


可是若是你相反的角度觀察(從裏向外而不是從外向裏),yield和next()之間的匹配就顯得更有道理。


回憶一下,yield ..表達式將被你用於繼續generator的值完成。這意味着你傳遞給next(..)的參數值將完成任何當前暫停中等待完成的yield ..表達式。


讓咱們這樣展現一下這種視角:

image.png


在這個代碼段中,每一個yield ..都送出一個值(1,2,3),但更直接的是,它暫停了generator來等待一個值。換句話說,它就像在問這樣一個問題,「我應當在這裏用什麼值?我會在這裏等你告訴我。」


如今,這是咱們如何控制*foo()來啓動它:

image.png


這第一個next()調用從generator初始的暫停狀態啓動了它,並運行至第一個yield。在你調用第一個next()的那一刻,並無yield ..表達式等待完成。若是你給第一個next()調用傳遞一個值,目前它會被扔掉,由於沒有yield等着接受這樣的一個值。


注意: 一個「ES6以後」時間表中的早期提案 將 容許你在generator內部經過一個分離的元屬性(見第七章)來訪問一個被傳入初始next(..)調用的值。


如今,讓咱們回答那個未解的問題,「我應當給x賦什麼值?」 咱們將經過給 下一個 next(..)調用發送一個值來回答:

image.png


如今,x將擁有值"foo",但咱們也問了一個新的問題,「我應當給y賦什麼值?」

image.png


答案給出了,另外一個問題被提出了。最終答案:

image.png


如今,每個yield ..的「問題」是如何被 下一個 next(..)調用回答的,因此咱們觀察到的那個「額外的」next()調用老是使一切開始的那一個。


讓咱們把這些步驟放在一塊兒:

image.png


在生成器的每次迭代都簡單地爲消費者生成一個值的狀況下,你可認爲一個generator是一個值的生成器。


可是在更通常的意義上,也許將generator認爲是一個受控制的,累進的代碼執行過程更恰當,與早先「自定義迭代器」一節中的tasks隊列的例子很是相像。


注意: 這種視角正是咱們將如何在第四章中重溫generator的動力。特別是,next(..)沒有理由必定要在前一個next(..)完成以後當即被調用。雖然generator的內部執行環境被暫停了,程序的其餘部分仍然沒有被阻塞,這包括控制generator何時被繼續的異步動做能力。


提早完成


正如咱們在本章早先講過的,鏈接到一個generator的迭代器支持可選的return(..)和throw(..)方法。它們倆都有當即停止一個暫停的的generator的效果。


考慮以下代碼:

image.png


return(x)有點像強制一個return x就在那個時刻被處理,這樣你就當即獲得這個指定的值。一旦一個generator完成,不管是正常地仍是像展現的那樣提早地,它就再也不處理任何代碼或返回任何值了。


return(..)除了能夠手動調用,它還在迭代的最後被任何ES6中消費迭代器的結構自動調用,好比for..of循環和...擴散操做符。


這種能力的目的是,在控制端的代碼再也不繼續迭代generator時它能夠收到通知,這樣它就可能作一些清理工做(釋放資源,復位狀態,等等)。與普通函數的清理模式徹底相同,達成這個目的的主要方法是使用一個finally子句:

image.png

警告: 不要把yield語句放在finally子句內部!它是有效和合法的,但這確實是一個可怕的主意。它在某種意義上推遲了return(..)調用的完成,由於在finally子句中的任何yield ..表達式都被遵循來暫停和發送消息;你不會像指望的那樣當即獲得一個完成的generator。基本上沒有任何好的理由去選擇這種瘋狂的 壞的部分,因此避免這麼作!


前一個代碼段除了展現return(..)如何在停止generator的同時觸發finally子句,它還展現了一個generator在每次被調用時都產生一個全新的迭代器。事實上,你能夠併發地使用鏈接到相同generator的多個迭代器:

image.png


提早停止


你能夠調用throw(..)來代替return(..)調用。就像return(x)實質上在generator當前的暫停點上注入了一個return x同樣,調用throw(x)實質上就像在暫停點上注入了一個throw x。


除了處理異常的行爲(咱們在下一節講解這對try子句意味着什麼),throw(..)產生相同的提早完成 —— 在generator當前的暫停點停止它的運行。例如:

image.png


由於throw(..)基本上注入了一個throw ..來替換generator的yield 1這一行,並且沒有東西處理這個異常,它當即傳播回外面的調用端代碼,調用端代碼使用了一個try..catch來處理了它。


與return(..)不一樣的是,迭代器的throw(..)方法毫不會被自動調用。


固然,雖然沒有在前面的代碼段中展現,但若是當你調用throw(..)時有一個try..finally子句等在generator內部的話,這個finally子句將會在異常被傳播回調用端代碼以前有機會運行。


錯誤處理


正如咱們已經獲得的提示,generator中的錯誤處理可使用try..catch表達,它在上行和下行兩個方向均可以工做。

image.png


錯誤也能夠經過yield *委託在兩個方向上傳播:


image.png


當*foo()調用yield 1時,值1原封不動地穿過了*bar(),就像咱們已經看到過的那樣。


但這個代碼段最有趣的部分是,當*foo()調用throw "foo: e2"時,這個錯誤傳播到了*bar()並當即被*bar()的try..catch塊兒捕獲。錯誤沒有像值1那樣穿過*bar()。


而後*bar()的catch將err普通地輸出("foo: e2")以後*bar()就正常結束了,這就是爲何迭代器結果{ value: undefined, done: true }從it.next()中返回。


若是*bar()沒有用try..catch環繞着yield *..表達式,那麼錯誤將理所固然地一直傳播出來,並且在它傳播的路徑上依然會完成(停止)*bar()。


轉譯一個Generator


有可能在ES6以前的環境中表達generator的能力嗎?事實上是能夠的,並且有好幾種了不得的工具在這麼作,包括最著名的Facebook的Regenerator工具 (https://facebook.github.io/regenerator/)。


但爲了更好地理解generator,讓咱們試着手動轉換一下。基本上講,咱們將製造一個簡單的基於閉包的狀態機。


咱們將使本來的generator很是簡單:

image.png


開始以前,咱們將須要一個咱們可以執行的稱爲foo()的函數,它須要返回一個迭代器:

圖片


如今,咱們須要一些內部變量來持續跟蹤咱們的「generator」的邏輯走到了哪個步驟。咱們稱它爲state。咱們將有三種狀態:起始狀態的0,等待完成yield表達式的1,和generator完成的2。


每次next(..)被調用時,咱們須要處理下一個步驟,而後遞增state。爲了方便,咱們將每一個步驟放在一個switch語句的case子句中,而且咱們將它放在一個next(..)能夠調用的稱爲nextState(..)的內部函數中。另外,由於x是一個橫跨整個「generator」做用域的變量,因此它須要存活在nextState(..)函數的外部。


這是將它們放在一塊兒(很明顯,爲了使概念的展現更清晰,它通過了某些簡化):

image.png


最後,讓咱們測試一下咱們的前ES6「generator」:

image.png


不賴吧?但願這個練習能在你的腦中鞏固這個概念:generator實際上只是狀態機邏輯的簡單語法。這使它們能夠普遍地應用。


Generator的使用


咱們如今很是深刻地理解了generator如何工做,那麼,它們在什麼地方有用?


咱們已經看過了兩種主要模式:


生產一系列值: 這種用法能夠很簡單(例如,隨機字符串或者遞增的數字),或者它也能夠表達更加結構化的數據訪問(例如,迭代一個數據庫查詢結果的全部行)。


這兩種方式中,咱們使用迭代器來控制generator,這樣就能夠爲每次next(..)調用執行一些邏輯。在數據解構上的普通迭代器只不過生成值而沒有任何控制邏輯。


串行執行的任務隊列: 這種用法常常用來表達一個算法中步驟的流程控制,其中每一步都要求從某些外部數據源取得數據。對每塊兒數據的請求可能會當即知足,或者可能會異步延遲地知足。


從generator內部代碼的角度來看,在yield的地方,同步或異步的細節是徹底不透明的。另外,這些細節被有意地抽象出去,如此就不會讓這樣的實現細節把各個步驟間天然的,順序的表達搞得模糊不清。抽象還意味着實現能夠被替換/重構,而根本不用碰generator中的代碼。


當根據這些用法觀察generator時,它們的含義要比僅僅是手動狀態機的一種不一樣或更好的語法多多了。它們是一種用於組織和控制有序地生產與消費數據的強大工具。

相關文章
相關標籤/搜索