這是一門在Udemy 課程,是同學介紹的。主要是因為想要運用到過年前這段時間好好的增進自己的JavaScript的基礎能力,讓自己能往前端工程師更近一步。主要還是會以筆記的形式做呈現!
在正式上課前,講師先給我們看了一段code:
1 | function greet(whattosay) { |
好像有點難懂,我們換另一種寫法:
1 | function greet(whattosay) { |
看起來好像很合理,但如果仔細一想,有一個疑問:為什麼sayHi
會知道whattosay
這個參數。
我的想法是:whattosay
是在我們called
greet function
時創建的,而當greet這個function執行完成後參數whattosay
應該會從execution stack
離開才對。為什麼sayHi
還能找到呢?
這就是我們要學的closures
所帶來的結果。
|Closures的底層原理
當我們執行整段code時,我們知道整段code的global execution context
會被建立。
當我們來到 sayHi = greet
時,他會invokes
greet這個function,創建新的execution context
當執行到function greet(whattosay)
時,javaScript引擎會注意到這裡有一個parameters
(參數),因此把他放到了execution context
裡。
當再往下執行後,發現了return
,因此回傳了後面整段function,所以整個greet()
就會從stack
彈出。
但要記得,我們說過每個execution context
在記憶體裡都會有一個空間,在正常情況下,JavaScript引擎會透過garbage collection
來清除內容,但在execution context抽離的當下,雖然execution context已經不在了,但裡面的變數還是儲存在那個記憶體位置。
當我們繼續往下執行到sayHi('Tony')
時,我們建立了一個給匿名函式的execution context,同時裡面帶有變數name。
當我們執行這個這個匿名函式的console.log(whattosay + ' ' + name)
時,JavaScript引擎就會過scope chain
的方式來尋找whattosay
這個變數。這時候雖然我們的greet這個function的execution context
已經不在了,但其實在這個記憶體位置仍然留有參照(reference),所以在greet function裡面所建立的函式仍然可以找得到whattosay這個變數。
到這邊就是整個closure的底層原理。
|Closure的練習
1 | function buildFunctions() { |
看到第一眼本來很直覺的想要說答案是0,1,2,但在仔細看了一遍後注意到arr裡面放的是function而不是number。所以console.log(i)
並不會在被執行,而是當我們程式走到fs[0]()
時,他才會去執行console.log(i)
,所以這時要套用到剛剛學習的觀念,透過scope chain
去找到i的值。
在buildFunctions
這個function執行for迴圈時,每當執行一次,就會把 function( ){console.log(i)} 儲存到陣列中,但要注意的是這時候這個被儲到陣列中的function並沒有執行(invoke),而是只是儲存在裡面而已,因為它沒有透過括號 ( ) 來執行;然後 i 會繼續累加,當 i 累加到3的時候,因為不符合 i < 3 所以會跳出迴圈。因此i就會是等於3,而arr會是長這樣:
1 | arr = [f0, f1, f2] |
因此當我們透過scope chain
去找i時就會得到i = 3,之後再帶入到程式碼裡面就會得到答案3。由於f0、f1跟f2都擁有同樣的outer environment reference,因此答案會是:3,3,3。
那如果這時候我們要讓他輸出的結果變成 0, 1, 2時,我們該怎麼做?
這邊提到了2個做法,第一個是使用let
,第二個就是使用IIFEs
。
先看let
:
1 | function buildFunctions() { |
透過let,可以讓每次跑的迴圈都建立在一個新的記憶體位置,因此最後指到的地方會是不一樣的,於是可以輸出0, 1, 2的結果。
使用IIFEs
就稍微複雜了:
1 | function buildFunctions() { |
(function(j){...})(i)
因為這段是IIFEs,所以他會直接被執行並且會把變數i帶到function裏面,這也就導致在一開始原本一樣的outer environment reference
變成不一樣,因此當我們要再去outer environment reference
找參數時,就會因為參數的值不同,得到的輸出就不會再是都一樣的。
|Closure的進階練習
1 | function makeGreeting(language) { |
閉包的使用當然有很多種,例如上面這段code,雖然比前一個練習更複雜,但我們只要記得:
每執行一次函式,就會產生一個新的execution context,即使有多個參數值被儲存在記憶體中,JavaScript引擎會自己找到屬於該execution context的變數。
丨Closure的總結
closure是JavaScript引擎的一種特性,並不是說你需要去創造它或執行它。透過closure這樣的特性,我們可以確保當我們在執行function的時候,JavaScript引擎能夠找到其相對應的變數,也就是說,不論某一個function是不是已經執行完畢,是不是已經抽離execution stack,JavaScript引擎仍然可以找到外面的變數。
參考文章:https://pjchender.blogspot.com/2016/06/closuresclosurescallback.html