閉包(Closure)

在理解閉包之前需要先理解

  • 自由變數(全域變數)
  • 作用域鍊 scope chain
  • 靜態作用域 static scope(lexical scope)
  • 動態作用域 dynamic scope

作用域(Scope)

「作用域就是一個變數的生存範圍,一旦出了這個範圍,就無法存取到這個變數」

範例 - 區域變數

1
2
3
4
5
6
function test(){
var a = 10
}
console.log(a)

// 印出結果: Uncaught ReferenceError: a is not defined

這裡因為a的作用域存在於function test裡面所以無法被印出

把區域變數變成全域變數

JS 裡有一種狀況會自動產生全域變數,那就是賦值給未宣告的變數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function test() {
n = 10; //n直接轉變成全域變數
console.log('hello');
}
test()

console.log(n);

function test2() {
console.log(n + 1);
}

test2();
console.log(window.n); // 從這段可以明白n是全域變數
// hello , 10, 11, 10

範例 - 全域變數

1
2
3
4
5
6
var I_am_global = 123
function test() {
console.log(I_am_global)
}
test()
// 印出結果:123

這邊的變數會寫在global裡面稱為全域變數任何地方都可以存取到它

小練習

這邊test()會印出100或是200?

1
2
3
4
5
6
7
8
9
10
11
var a = 100
function echo() {
console.log(a) // 100 or 200?
}

function test() {
var a = 200
echo()
}

test()

很混淆對嗎?

但是只要明白:

  1. 當函式內部找不到變數使用時會往外部找
  2. 這邊的練習echo的外部就是global並不是test()
  3. 因此答案是100

靜態作用域(static scope)

代表作用域跟這個 function 在哪裡被「呼叫」一點關係都沒有,你用肉眼看程式碼的結構就可以看出來它的作用域是什麼,而且是不會變的。

以小練習的範例說明的話:

  1. 在test裡面另外宣告了a 並且呼叫了它
  2. 但是因為靜態作用域的影響 function 被宣告時就被決定了它的外部環境也就是全域變數
  3. 所以執行階段的出現的a 變數(test內)並沒有影響其值
  4. 但是如果JS採用的是動態作用域的話印出結果就會是200

閉包(Closure)

範例說明閉包

1
2
3
4
5
6
7
var my_balance = 999
function deduct(n) {
my_balance -= (n > 10 ? 10 : n) // 超過 10 塊只扣 10 塊
}

deduct(13) // 只被扣 10 塊
my_balance -= 999 // 還是被扣了 999 塊

就算我們函式操作好每次扣的金額,但是因為變數在全域範圍,因此任何人都可以取用這個時候就可以使用閉包

1
2
3
4
5
6
7
8
9
10
11
12
13
function getWallet() {
var my_balance = 999
return {
deduct: function(n) {
my_balance -= (n > 10 ? 10 : n) // 超過 10 塊只扣 10 塊
}
}
}

var wallet = getWallet()
wallet.deduct(13) // 只被扣 10 塊
my_balance -= 999 // Uncaught ReferenceError: my_balance is not defined

這邊透過return的方式把函式包在另一個函式內部的物件內,透過這樣的方式想要修改變數my_balance就會失敗搂!這樣的使用方式就是閉包

範例說明閉包二

假設html有五個按鈕

點擊按鈕後會得出甚麼結果?

1
2
3
4
5
6
7
var btn = document.querySelectorAll('button')
for (var i = 0; i <= 4; i++) {
console.log(i);
btn[i].addEventListener('click', function () {
alert(i)
})
}

結果是都是5,並不是預期中0,1,2,3,4

原因如下:

  1. 事件會從stack被丟入web API 做處理
  2. 所以這邊console.log(i)就已經先印出0~4了
  3. 迴圈跑到5的時候跳出迴圈這時候事件的部分也從event loop回來接受i的資料
  4. 因此印出的內容都是5

解決方式:

使用let代替var,每次跑回圈都會產生新的作用域,因此alert出來就會是想要的值了

1
2
3
4
5
for(let i=0; i<=4; i++) {
btn[i].addEventListener('click', function() {
alert(i)
})
}