JavaScript Closure


Scope Chain

在開始談閉包之前,我們現來談談「範圍鏈」(Scope Chain) 的觀念。

Scope Chain(範圍鏈)的特性,是指使用變數的時候,會遵循著 Scope Chain 一層一層往外找,也就是看看函式自身的 context 物件上是否有該特性,如果沒有就往外頭的 context 物件看看有沒有該特性。

用 var 所宣告的變數,作用範圍是在當時所在環境(函式內),而不使用 var 直接指定值而建立的變數,則是全域物件(window)上的一個屬性,也就全域範圍。

例如像下面這段程式碼:

1
2
3
4
5
6
7
var myVar = "outer";
function func(){
console.log(myVar); // outer
myVar = "inner";
}
func();
console.log(myVar); // inner

func 的變數myVar 因為沒有使用 var 進行宣告,所以 myVar 會變成「全域變數」。

修改一下上面範例,若是在func 中的 myVar,有透過 var 宣告時,變數就會作用在當時的環境,也就是 func

由於 JavaScript 提升 (Hoisting)的特性,Hoisting 是 JavaScript 的預設行為,把所有宣告效果提到當前 Scope 的頂端,也就是變數可以在宣告之前進行初始化和使用,而不會拋錯:

1
2
3
4
5
6
7
var myVar = "outer";
function func(){
console.log(myVar); // undefined
var myVar = "inner";
}
func();
console.log(myVar); // outer

func 運作上等同於:

1
2
3
4
5
function func(){
var myVar;
console.log(myVar);// undefined
myVar = "inner";
}

讓我們在看一個例子,每一個 function 執行的時候都會建立一個新的 context,所以 funcfund 各自為獨立的一個 Function execution context(執行環境):

1
2
3
4
5
6
7
8
9
10
11
12
var myVar = "outer";
//另外一個執行環境
function func(){
console.log(myVar); // outer

}
//一個執行環境
function fund(){
var myVar = "inner";
func();
}
fund();

fund 呼叫 func 時,由於 fundfunc 都處於全域的環境,而 myVar 變數是被定義在 fund 的函式裡面;當你試圖在 func 中 使用 myVar 變數時,它會查看 func 的 context 物件上是否有該特性,如果有就使用,沒有就往外面一層找。由於 func 的上一層是 global context ,所以就存取到全域變數的 myVar,最終印出的結果就會是 outer


如果函式 func 的位置是被 fund 所包裹,當 func 找不到 myVar ,就會往它的向外一層找;所以就存取到 fund 裡面的 myVar ,輸出 inner

1
2
3
4
5
6
7
8
9
10
11
12
var myVar = "outer";
function fund(){
var myVar = "inner";
func();

function func(){

console.log(myVar); // inner
}

}
fund();

閉包 Closure

閉包是個特殊的物件,他包含了一個函式,以及函式被建立時所在的環境。

在 JavaScript 中,只要有巢狀的函數定義,就會形成閉包。因為內層的函數需要引用到外層函數中定義的變數(建立範圍鏈 Scope Chain),所以外層函數中變數的狀態就好像被內層函數「關閉」起來了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function OuterFunction() {
// 一個局部變數
var outerVariable = 100;

function InnerFunction() {
// 內部函數可以存取外部函式的變數

alert(outerVariable);
}

// 返回一個內部函式,並創建一個 closure
return InnerFunction;
}
var innerFunc = OuterFunction();

innerFunc(); // 100

上面例子中,OuterFunction() 執行時返回一個 function,同時自動創建了一個 closure 環境。closure 像是一個特殊的物件 (指定給了 innerFunc ),closure 中包含一個函數 InnerFunction ,以及函數 OuterFunction 執行當時的環境,讓你在函數返回後還是可以持續存取 closure 保存的環境 ,像是能存取 outerVariable 變數, outerVariable 變數不會因為函數返回後而被刪除。


在這個例子中,我們把 counter 封裝在 Counter() 當中,不但可以讓裡面的 counter 不會暴露在 global 環境造成變數衝突,也可以確保內部 counter 被修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Counter() {
var counter = 0;

function IncreaseCounter() {
return counter += 1;
};

return IncreaseCounter;
}

var counter = Counter();
alert(counter()); // 1
alert(counter()); // 2
alert(counter()); // 3
alert(counter()); // 4

如果我們需要新增另一個計數器的話,透過一個閉包可以很輕鬆地達成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Counter() {
    var counter = 0;

    function IncreaseCounter() {
        return counter += 1;
    };

return IncreaseCounter;

}
var counter = Counter();
var counter2 = Counter();

alert(counter()); // 1
alert(counter()); // 2
alert(counter()); // 3

alert(counter2()); // 1
alert(counter2()); // 2

countercounter2 各自是「獨立」的計數器實體,彼此不會互相干擾。

參考文獻

  1. https://ithelp.ithome.com.tw/articles/10193009

  2. https://www.fooish.com/javascript/function-closure.html

  3. https://ithelp.ithome.com.tw/articles/10029457

  4. https://ithelp.ithome.com.tw/articles/10199934

  5. https://www.tutorialsteacher.com/javascript/closure-in-javascript