Scope Chain
在開始談閉包之前,我們現來談談「範圍鏈」(Scope Chain) 的觀念。
Scope Chain(範圍鏈)的特性,是指使用變數的時候,會遵循著 Scope Chain 一層一層往外找,也就是看看函式自身的 context 物件上是否有該特性,如果沒有就往外頭的 context 物件看看有沒有該特性。
用 var 所宣告的變數,作用範圍是在當時所在環境(函式內),而不使用 var 直接指定值而建立的變數,則是全域物件(window)上的一個屬性,也就全域範圍。
例如像下面這段程式碼:
1 | var myVar = "outer"; |
func
的變數myVar
因為沒有使用 var
進行宣告,所以 myVar
會變成「全域變數」。
修改一下上面範例,若是在func
中的 myVar
,有透過 var
宣告時,變數就會作用在當時的環境,也就是 func
。
由於 JavaScript 提升 (Hoisting)的特性,Hoisting 是 JavaScript 的預設行為,把所有宣告效果提到當前 Scope 的頂端,也就是變數可以在宣告之前進行初始化和使用,而不會拋錯:
1 | var myVar = "outer"; |
func
運作上等同於:
1 | function func(){ |
讓我們在看一個例子,每一個 function 執行的時候都會建立一個新的 context,所以 func
和 fund
各自為獨立的一個 Function execution context(執行環境):
1 | var myVar = "outer"; |
當 fund
呼叫 func
時,由於 fund
和 func
都處於全域的環境,而 myVar
變數是被定義在 fund
的函式裡面;當你試圖在 func
中 使用 myVar
變數時,它會查看 func
的 context 物件上是否有該特性,如果有就使用,沒有就往外面一層找。由於 func
的上一層是 global context ,所以就存取到全域變數的 myVar
,最終印出的結果就會是 outer
。
如果函式 func
的位置是被 fund
所包裹,當 func
找不到 myVar
,就會往它的向外一層找;所以就存取到 fund
裡面的 myVar
,輸出 inner
。
1 | var myVar = "outer"; |
閉包 Closure
閉包是個特殊的物件,他包含了一個函式,以及函式被建立時所在的環境。
在 JavaScript 中,只要有巢狀的函數定義,就會形成閉包。因為內層的函數需要引用到外層函數中定義的變數(建立範圍鏈 Scope Chain),所以外層函數中變數的狀態就好像被內層函數「關閉」起來了。
1 | function OuterFunction() { |
上面例子中,OuterFunction()
執行時返回一個 function,同時自動創建了一個 closure 環境。closure 像是一個特殊的物件 (指定給了 innerFunc
),closure 中包含一個函數 InnerFunction
,以及函數 OuterFunction
執行當時的環境,讓你在函數返回後還是可以持續存取 closure 保存的環境 ,像是能存取 outerVariable
變數, outerVariable
變數不會因為函數返回後而被刪除。
在這個例子中,我們把 counter
封裝在 Counter()
當中,不但可以讓裡面的 counter
不會暴露在 global 環境造成變數衝突,也可以確保內部 counter
被修改:
1 | function Counter() { |
如果我們需要新增另一個計數器的話,透過一個閉包可以很輕鬆地達成:
1 | function Counter() { |
counter
與 counter2
各自是「獨立」的計數器實體,彼此不會互相干擾。