壹、範圍鏈 Scope Chain
以 function 為界線,在 function 內層如果找不到變數可以一直向外層找變數,但 function 外層不能讀取到 function 內層的變數。
「切分變數有效範圍的最小單位是 “function” 」
但如果你無論如何都想要讀取到要怎麼辦呢? 這時候就不得不提閉包了。
閉包除了可以 在外層讀取到內的的變數 外,
還可以建立屬於自己的異世界 儲存建立環境時的變數值。
趕快來探索這個異世界的奧秘吧
二、閉包
因為 function 內部的變數無法被外部讀取,因此在使用 function 時常常需要在外層宣告了很多變數,如果是大型的案子可能會造成衝突。
以這個程式碼為例,變數都必須先宣告在函式外
1 | var LeoTeam = 4 // Leo 的團隊有 4 個人 |
如果我不想預先宣告變數,並且變數不需要宣告為全域變數,我可以…
使用閉包
1. 工廠模式
如此一來不管要創造幾個團隊,或是這個團隊的初始是多少人,後續要增加多少人,都可以很有彈性的變動,而這個方法又稱為閉包的 工廠模式,顧名思義就像工廠生產線一樣,很快速地去產出一個團隊。
1 | function team(initNum){ |
2. 私有方法
另外介紹一下 私有方法,比較像針對特定對象,去做客製化處理。
像下面這個例子,就可以去將團隊的人數作增減,不只是多幾人,也可以少幾人。
私有方法是將 多個函式寫在 return 中,選定需要的函式功能去執行。
1 | function team(initNum){ |
3. 閉包的經典題目
下面這段程式碼,大家預期 fn[0]、fn[1]、fn[2] 會出現什麼數字呢?
如果覺得是 0、1、2 的話….你再想想~~
1 | function arrFunction(){ |
.
.
.
.
.
.
.
.
.
.
.
答案會是 3、3、3 喔~
為什麼會是 333,這不合理阿!
讓我慢慢道來
我們可以將這個程式碼分為兩段來看
第一段是 var fn = arrFunction();
這裡就會執行了 for 迴圈,因此這邊的 i 就已經跑完了,
你或許會有疑問,i 不是小於 3 嗎? 為什麼最後 i 會跑到 3,
這裡可以把 for 想成一個條件式,i 的值小於 3 他就會繼續跑下一個迴圈,
但如果 i 等於 3 時,就會離開這個迴圈,但這時 i 的值就會是 3
第二段是 fn[0]()
console.log(i)會在這邊執行
記得第一段的 i 的值目前這個狀態是多少嗎??
對,就是 3 喔。
因此當 fn[0] () 去找 i 的值的時候,在他的內部找不到 i,
因此透過範圍鏈到之前 fn = arrFunction() 的執行環境建立的記憶體位置去找 i,
這時找到的 i 就會是 3。
之後 fn[1] () 、fn[2] () 一樣會到同一個記憶體位置找到 i = 3。
因此,最後 console.log 出來的值會是 333。
那有什麼辦法可以解決呢??
在這邊提供兩個解法:
1. 使用 ES6 的 let
let 宣告變數的方法,其中一個特性是,let 的作用域 (scope) 會改以 {} 為範圍,而不是傳統的 var 的作用域為 function(){..} 的大括號內為範圍。
因此在執行第一段程式 var fn = arrFunction(); 時,每次 for 迴圈執行的 i 值都會被存到不同的記憶體內,這三個不同的記憶體位置裡面的 i 分別為 0、1、2,
( 對照原本使用 var 時,因為作用域是在 function 內,記憶體只會被放到同一個位置裡面。 )
當執行 fn[0] ()、 fn[1] () 、fn[2] () 時,就會到這三個記憶體位置取出三個不同的 i 值。
1 | function arrFunction(){ |
2. 使用 立即函式 IIFL
透過立即函式閉包的方式,
在第一段程式 var fn = arrFunction();
在跑迴圈的當下,因為立即函式的關係,跑一次迴圈,就建立一個自己的執行環境,跑三次就建立三個不同的執行環境,而每個執行環境都有自己的記憶體位置,i值就會存在這裡。
因此,執行第二段程式時,
fn[0] ()、 fn[1] () 、fn[2] (),他會去跑
return function(){
console.log(j)
}
而其中的j值,會分別從三個不同的記憶體中去找。
1 | function arrFunction(){ |
參考資料:
重新認識 JavaScript: Day 19 閉包 Closure
六角學院: JavaScript 核心篇
JavaScript 全攻略:克服 JS 的奇怪部分