閱讀本篇文章前,仔細想想看
- 同步與非同步程序的差異性在哪?你能夠列舉哪些是 JS 裡有非同步的機制的東西嗎?
- 為何我們需要非同步的程序?同步執行不是很直觀嗎?
- 回呼函式地獄(Callback Hell)可以用什麼方式解決?
另外,本篇承載前一篇文章的內容,強烈建議如果跳到這一篇的讀者務必把前一篇看完啊!
今天要講比較麻煩一點的東西,也許是本系列最難的部分吧!
相信讀者經歷過這一段過程,應該就連 ES6 Generators 的語法精髓都順便學完了。
以下正文開始。
事實上,從這裡開始,筆者要順便介紹 —— ES6 Generators 在搞什麼,不過在理解這個東西前,筆者建議讀者好好看過迭代器模式篇章,裡面的內容有涵蓋到 Generator Function 的特性,因為:
Generators 可以看作是 JavaScript 版本的產生迭代器的 Factory Method。
『恩!?好像看得懂又好像看不懂!?』
貼心小提示
若想要追求名詞的精確性 —— ES6 Generator Functions 儘管被筆者形容成可以看作為迭代器模式中的 Factory Method 部分,但它並不是用 OOP 裡面的類別的成員方法來達成,而是單純的一個函式作為所謂的 Factory Method;不過 Method 這個詞的定義依然跟 Function 有差,所以 Generator Functions 應該要看作為迭代器的 Factory Function 而非 Method。
但是! —— Design Pattern 原著裡,因為本著 OOP 的實踐基底,所以並沒有所謂的 Factory Function 這一套說法 —— 這個詞是筆者產出來的延伸物,不是那些大神們的說法,筆者只是小蝦米而已。
事實上,學過 Python 的人應該也有接觸過 Generators,如果你有這樣的背景,事實上你早就知道大部分 JavaScript 的 Generators 的運作機制,差別僅僅在於 —— 如果迭代器已經到尾端,繼續呼叫 next
方法時,Python 會丟出類似 Out of bound
之類的 Exception
,而 JavaScript 不會。這一段如果看不懂也沒差,因為我們在講 JavaScript,而不是一條滑滑的爬蟲類動物。XDDDDD
回過頭來,筆者開始介紹 Generator Functions,而且會結合 TypeScript 的型別系統推論機制 —— 這可是本系列天大的重點啊!啊!啊!啊!啊!(其實筆者只要感嘆一次就好)
以過往的慣性,就從最簡單的 Generator Function 的範例出發:
沒看過的讀者云:“嘖嘖,麻煩死了,那麼多東西要學。”
這裡劇透一下,為了追求更好的寫法 —— 尤其是 ES7 Async Await 的 Feature,筆者自從會了這東西,突然覺得寫 JS 的過程,前途一片光明(請不要誇大) —— 但在這之前,必須要先好好介紹 ES6 Generators。
回過頭來,以上的 numbersIteratorGenerator
為產生某種迭代器的函式,使用情形如下。(程式碼結果如圖ㄧ)
圖ㄧ:numbersIter
不停呼叫 next
方法時,輸出的格式皆為 { value: number | undefined, done: boolean }
筆者把結果呈現出來:
不覺得很像迭代器模式篇章提過的 —— 迭代器的長相嗎?(這句應該是廢話)
而這裡將迭代器的元素邏輯 —— 用一個函式的流程,將迭代的元素改成用 yield
關鍵字進行輸出的動作;並且,每個元素輸出時的結構為普通的物件搭配 value
與 done
這兩個屬性 —— 分別代表當前迭代元素之值以及迭代器的狀況(迭代完畢時為 true
)。
不過這裡還是要雞婆的提醒讀者:這個 Generator Function 的 Feature 不是 TypeScript 提供的,這是列為 ECMAScript 標準的語法!(這句應該也是廢話)
重點 1. ES6 Generator Functions
若想要建立一個迭代器產生函式(Generator Functions),可以宣告一個 ES6 Generator 函式,並且必須在
function
以及函式名稱之間註記一個*
字號 —— 而迭代器的元素可以使用yield
關鍵字進行輸出:以下簡稱 Generator Functions 為 Generators。
若想要從 Generators 建立一個迭代器(Iterator),可以直接呼叫該函式,視情況填入函式之參數。
每一次迭代一個新的元素必須呼叫
next
方法:每一次迭代出來的元素結構為:
其中,若迭代器還未迭代完畢,
done
的狀態為false
,反之為true
。(天大廢話一籮筐)
由於本 TypeScript 系列要探討的重點跟型別系統有關的東西,因此筆者就把語法基本層面 —— 讀者可能必須要知道的事項放在讀者試試看的部分。畢竟筆者可以選擇直接把 MDN 或其他篇文章已經產出的語法教學複製貼到這裡來,但筆者覺得,改成用測試的方式看看讀者能不能在學習過程中注意到細微的點,如果讀者主動測試過,筆者也不需要耗費太大力氣,讀者也至少會有印象,碰過這些東西。
讀者試試看
請讀者親自試試看或者查個資料也行,看能不能回答以下問題:
- 試問以下
generator1
與generator2
差別在哪?
- 假設我們有一個
generatorFunc
為一個 Generator Function,試問從該函式剛建立一個迭代器時,裡面的console.log
會跑到嗎?還是要等到呼叫第一個next
才會跑到呢?
- 試問以下的 Generator 會產生什麼樣的效果?
很簡單,直接用滑鼠 Point 到 Generator 函式位置就會出現提示,舉剛剛的 numbersIteratorGenerator
為例,它的推論結果如圖二。
圖二:輸出好長...
以下筆者整理一下輸出:
推論結果裡出現一個很陌生的東西 —— Generator<X, Y, Z>
型別,該泛用型別有三個型別參數,這可真是複雜,但是請讀者謹記,我們還是可以繼續鑽下去查看 Generator
的型別宣告(Type Declaration)部分。(如圖三)
圖三:Generator
的型別定義(Type Definition)
原來 Generator
是一個介面 —— 擴充自 Iterator<T, TReturn, TNext>
。(參見介面擴充篇章)
這時筆者就要得意的說,看到了沒 —— 這就是筆者所謂的 TypeScript 的好處:
筆者完全不需要上網查詢,直接在編輯器裡就可以查到陌生的型別的規格內容,並且理解內部的結構
筆者就把規格拔下來給讀者看:
ES6 Generators 迭代器的結構就被筆者曝光了!
next
方法就是輸出 TNext
型別的東西
return
這個方法,但筆者個人覺得 —— 由於沒用到所以不再介紹,讀者可以自行研究throw
方法看起來是錯誤情形時使用的方法[Symbol.iterator]()
—— 這東西筆者賣個關子,覺得後面講到 Symbol 以及 for...of...
迴圈可以補充反正 Generator
的參數分別為 T
、TReturn
以及 TNext
,而剛剛的 numbersIteratorGenerator
輸出之結果為對應為:
T
為 1 | 2 | 3 | 4 | 5
TReturn
為 void
TNext
為 unknown
筆者這邊大致猜得出來,T
指得是所有從迭代器 yield
出來元素的 union
;TReturn
則是因為我們的 Generator 函式內部沒有使用 return
表達式,所以才會為 void
。
TNext
這個東西,筆者下一篇會再講到為何是 unknown
型別。
所以如果我們正常使用 Generator 的迭代器,推論結果會很有趣。(如圖四)
圖四:IteratorResult<1 | 2 | 3 | 4 | 5, void>
如果再查 IteratorResult
這個介面會長這樣。
這應該更明顯了吧:一個是指 yield
出來的元素型別、另一個則是 return
出來的型別,兩個 union
起來的結果!
所以呼叫 next
方法輸出的結果,如果迭代器還沒迭代結束,就會是 1 | 2 | ... 5
型別,而如果迭代結束就會成為 void
型別 —— 但迭代結束實際上是輸出 { value: undefined, done: true }
這個物件。
接下來要繼續講述 ES6 Generators 的特點,以及 Generators 在 ECMAScript 標準裡很重要的原因。
事實上,從上一篇解剖 Generator<T, TReture, TNext>
這個型別背後的介面長相,筆者必須要把這一行特別 Highlight 出來給大家看:
這一行 next
方法的結構是 next(...args: [] | [TNext]): IteratorResult<T, TReturn>
—— 先不要被這個結構嚇到,...args: [] | [TNext]
的意思代表 —— 匯集所有在 next
方法裡的參數只能為兩種情形:
[]
代表沒有任何參數[TNext]
代表只能存在一個參數符合 TNext
的型別(別忘了,這是元祖型別!代表該陣列只會有一個元素,此元素型別為 TNext
,剛開始會搞混也是很正常!)讀者有想過,我們從 Generators 建構出的迭代器,使用 next
方法時傳入參數的意思是什麼呢?
筆者就來簡單的方式呈現,以下為 summationGenerator
迭代器產生函式:
讀者看到這一行應該覺得很怪,以下這一行:
total += yield total;
這是什麼意思?
首先,筆者先把 summationGenerator
產生的迭代器,使用起來的效果給讀者看看。(以下程式碼結果如圖五)
圖五:summationGenerator
產生的迭代器 summationIter
使用起來的效果
連續呼叫三次且分別在 next
方法傳入數字 5
、7
與 11
—— 結果出來的結果依序為 0
、7
、18
。
其實筆者舉這個迭代器函式的用意是希望可以進行累加的功能,不過讀者應該覺得奇怪,第一個迭代出來的數字結果為何不是 5
而是 0
。
這裡就不賣關子,直接說明迭代器產出函式及其產出的迭代器的 next
方法特點:
重點 2. Generators 的性質
- 從 Generators 產出的迭代器不會馬上動作
- 執行過程上:迭代器呼叫第一次的
next
方法,該迭代器就會執行到 Generator 函式的第一個yield
位置;亦或者,如果沒有yield
,就會執行到return
為止- 迭代過程上:迭代器呼叫第一次的
next
方法,並且執行到第一次yield
的位置時,會將yield
旁邊的值(可能也包含undefined
)進行輸出的動作;如果沒有yield
則是會將return
的結果值輸出- 迭代器只要在第 n 次呼叫
next
方法並且代入任何值,該值就會取代前一個yield
關鍵字的位置
這邊很複雜,讀者看不懂也沒問題,直接看筆者分解過程後,再回來看重點 1 應該會很清楚筆者想講什麼。
首先,第一次(n = 1)呼叫 summationIter.next(5)
時,由於是第一次呼叫,但此時迭代器的初始地點,也就是函式的一開始根本沒有 yield
關鍵字,也就是說 5
這邊有填沒填都沒差。(如圖六)
圖六:第一次呼叫 next
是在函式的開端,根本不會有 yield
關鍵字供數字 5
取代
所以剛開始 total = defaultValue
,而呼叫 summationGenerator
由於沒有填入參數,因此預設為 0
,所以 total = 0
—— 遇到第一次的 yield total
時,輸出為:
{
value: 0,
done: false
}
第二次(n = 2)呼叫 summationIter.next(7)
時,由於是第二次呼叫,它會將第一次呼叫 next
跑到的 yield
位置取代為數值 7
;也就是說,將 yield total
看成 7
這個數字,就會變成:
while (true) {
total += 7; // 原本是 yield total;
}
然後繼續執行直到遇到下一個 yield
,不過剛執行完 total += 7
時,就已經碰到迴圈的底,因此重新執行迴圈,又遇到一次 total += yield total
,於是停住 —— 此時的 yield total
出來的值是原本的 defaultValue = 0
再加上第二次呼叫 next(7)
的數值 —— 0 + 7 = 7
,因此第二次輸出結果為:
{
"value": 7,
"done": false
}
第二次輸出後就停掉,直到第三次(n = 3)我們呼叫 summationIter.next(11)
時,我們要把第二次跑到的 yield total
的值,更改成第三次呼叫 next
時的輸入值 11
,所以可以看成:
while (true) {
total += 11; // 原本是 yield total;
}
然後開始跑,此時的 total += 11
為 7 + 11 = 18
,而剛好接下來執行時又碰 while
迴圈的底,於是重跑,又撞到第四次 yield total
—— 此時的 total = 18
,因此輸出結果為:
{
"value": 18,
"done": false
}
所以呢,讀者應該可以感覺到:
第
n
次呼叫next
方法時,會執行到第n
個yield
位置
這應該很直觀,但是:
第
n
次呼叫next
方法時,填入的參數值會取代第n - 1
的yield
位置
這觀念非常重要,而且我們執行第一次 next
時,如果有填入參數,但是想要取代 n - 1 = 0
的 yield
位置是不可能的事情,因此筆者才會說:第一次呼叫 next(5)
時,裡面的 5
這個數字有填沒填都沒差 —— 通常第一次呼叫 next
大部分情形都有點像是在初始化(Initialize)迭代器內部的過程。
好的,筆者要再給予這個 Generator 的這個特性兩個名詞:
next
方法時,從迭代器取出值的過程,稱之為 PULL
next
方法並且填入參數的這個行為,就好像是在迭代器這個黑箱子外面提供資訊輸入進去,以改變迭代器後續的輸出結果樣貌,這樣的輸入行為稱之為 PUSH
這樣的行為,讀者可能覺得還好,但是筆者要強調:
重點 3. Generators 的優點
普通的迭代器模式中的迭代器是靜態(Static)的,代表內部的值是固定的 —— 每一次從迭代器模式的 Factory Method 建立出的迭代器都會是固定的。
然而,ES6 Generators 的特點在於 —— 藉由 Push-Pull 的機制,可以達到迭代出來的元素可以根據不同的情形更改元素輸出的狀態,因此可以將 Generators 產出的迭代器視為動態(Dynamic)的。
筆者 O.S.:之前講解 OOP 迭代器模式不是沒理由的講解,而是可以比對 Generators 的迭代器與 OOP 迭代器模式的差別在哪裡。
另外,讀者應該也會發現,在 summationGenerator
和前一篇有講到的 fibonacci
Generator 裡,筆者有用到 while
的無窮迴圈 —— 這裡筆者必須要講到另一個動態迭代器的特點 —— 惰性求值(Lazy Evaluation)的概念。
首先,讀者有沒有想過,有沒有辦法表示一個數列代表所有的正整數?
讀者可能覺得筆者在開玩笑:“電腦的 Memory 又沒辦法承載所有的數字,哪有辦法說使用陣列或 Space Complexity 更大的 Linked List 來表示?”
不過,設想一個情境,如果我們想要使用這種數列,但是並不是馬上使用,而是需要的時候再用呢?
於是,想當然以下的 positiveNumberGenerator
定義出的迭代器行為就可以達到剛剛所說的目標:
以上的 positiveNumberGenerator
,同時滿足:
跟普通使用陣列的情形:
[1, 2, 3, 4 ... n, n + 1, ...]
完全是不ㄧ樣的 —— Generators 產出的迭代器 —— 需要用到值的時候,呼叫 next
方法就可以了,是不是聽起來懶惰了些?;而陣列由於必須積極地把值寫出來,因此一開始就得完整表示出值來。
重點 4. 惰性求值 Lazy Evaluation
惰性求值的概念在於 —— 需要用到值的時候,再把值求取(Evaluate)出來。
好處在於,除了可以表示無窮元素列表的概念外,如果遇到運算負荷重,但不需要馬上處理的邏輯時,可以採取這種惰性求值的策略 —— 需要處理時再去處理。
除了 ES6 Generators 產出的迭代器具有惰性求值的性質外,另一個是在本系列提到的單例模式篇章(Singleton Pattern)中,如果遇到建構單子耗費的資源稍微龐大但又不太需要即時建構的情形,就可以延遲建構單子,此為懶漢模式。
相對於惰性求值的行為,以下筆者也列出積極求值(Eager Evaluation)的特性。
重點 5. 積極求值 Eager Evaluation
積極求值相對於惰性求值 —— 是即時運算的行為。
一般程式語言的任何表達式(Expressions),基本上都是積極求值的行為,如:
- 指派表達式(Assignment Expression):
let a = 1
會立刻將數值1
指派到變數a
身上(可以查看關於 Memory Allocation 相關的行為)- 運算表達式(Arithmetic Expression):
3 + 5
會立刻求值為結果8
- 邏輯表達式(Logical Expression):
10 >= 8
會立刻比對數值10
是否大於等於8
,立馬求值的結果為true
- 函式/方法的呼叫(Function/Method Invocation):
Math.pow(2, 3)
會立刻運算出結果8
積極求值的優點在於事情一到就會立刻進行,但同時也是缺點的地方在於,如果資源過大,很容易會有程式卡住的情形。
由於篇幅問題(已經飆到“剛好” 13,000 字,筆者吃驚!),因此打算把剩餘的東西下一篇再說明。
ES6 Generators 應該是本系列最麻煩的東西,吸收本篇很需要時間。
另外,筆者還沒講到 Promise Chain 演變到 Generator 的寫法,到底會是什麼呢~ 請讀者繼續看下去喔~~~