0%

js核心篇-作用鍊和閉包

這篇要來記錄一下JS的作用鍊和閉包一些特性。
那如果要先來談作用鍊的話,我們一定要先講作用域的部分。

作用域

引用Huli文章裡面有關作用域的解釋,作用域就是變數存在的範圍,依但變數脫離這個範圍,我們就無法存取這個變數了。
而在es6之前,你要創造一個作用域的話,只要創造一個function,而這個function的內部環境就是一個作用域。

1
2
3
4
function funcA(){
var a = 10;
}
console.log(a); // Error, a is not defined

上面這個範例就可以看出來,JS變數的最小有效範圍是function,這個funcA就是這個變數的作用域範圍,一旦脫離了funcA這個作用域後,就無法再調用這個變數a囉。

範圍鍊

範圍鍊牽涉了以下兩個特性

  1. 在JavaScript的變數的最小有效範圍是function。
  2. 若在自己的層級找不到該變數的話,就會一層一層往外查找,直到找到Global為止,如果還是找不到的話,就報錯。
  3. 因為,在JavaScript中的function是屬於靜態作用域,所以,當函式被定義的當下,其範圍鍊就會順便被定義下來了,而不是在呼叫該函式時,才動態地定義該函式地範圍鍊,
    但是,JavaScript的this,就是屬於動態作用域,因為,它是被呼叫的時候,才被定義。

很好詮釋範圍鍊設定的範例

1
2
3
4
5
6
7
8
9
10
11
var a = 100;
function funcA() {
console.log(a); // 答案是100 或 200?
}

function funcB() {
var a = 200;
funcA();
}

funcB();

上面這個例子我覺得可以很好的詮釋什麼叫函式的範圍鍊是在定義的時候,就被定義完成了。
首先,funcA函式定義的部分,它的範圍鍊就是它自己的作用域和全域變數的作用域。
第二,所以,當funcA在funcB被呼叫的時候,因為,它的範圍鍊已經被定義好了,所以,它的a就是全域變數的a,也就是100囉。

閉包(Closure)

閉包像是一種特殊的物件,它會包含了當下該函式當下的執行環境,讓你在函式返回之後,還可以持續存取閉包保存住的執行環境。
而能造成閉包這種現象的原因,是因為我們在一個function裡面再回傳出一個function,才能造成明明外部的函式已經執行完畢,但是,內部的環境還能被保持住的原因。

閉包的好處

閉包的好處在於能夠避免包在函式內的變數被外部的變數給汙染到。
這邊一樣來的經典的範例

上面這個範例你可以看到,在createWallet的函式裡面有一個區域變數money,我們利用閉包的特性讓它無法被外界汙染,
只要ming和grey不被消滅掉,它們所儲存的閉包環境就會永遠存在喔~
而且,閉包之間是互相獨立的像是 ming 和 grey,並不會互相影響。

閉包經典範例

然後,這邊也是紀錄一下Huli文章中所提供的一個很經典的範例。
我們利用for迴圈來為每個按鈕綁定一個監聽事件,當我們點擊按鈕,它會跳出相對應的數字。
這邊先寫下錯誤的寫法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
---HTML---
<div>按鈕一</div>
<div>按鈕二</div>
<div>按鈕三</div>
<div>按鈕四</div>
<div>按鈕五</div>

---JavaScript---
let fragment = document.querySelectorAll('div');
for(var i=0; i!=fragment.length; i++) {
fragment[i].addEventListener('click', function() {
alert(i);
});
}

上面這個範例我們預設美個按鈕當會綁定到不一樣的i值,但事實不然,當你的綁定結束之後,你去點擊每個按鈕,你會發現他們都跳出5 ?!!!
哪尼?!!!
原因是,它你在做事件監聽的綁定的時候,你就只是定義了每個按鈕相對應的函式,你並沒有觸發他們的點擊事件,他們自然也不需要向外尋找變數i阿,
等到你要點擊它們的時候,此時,監聽事件中的i會向外查找,此時的i早就已經是加總完的數值了,這也就是為什麼點擊的結果都會跳出5的原因囉~

解決辦法

way1. 使用閉包
利用閉包來儲存當下的迴圈的i值

1
2
3
4
5
6
7
8
9
function getAlert(num) {
return function(){
alert (num);
}
}
let fragment = document.querySelectorAll('div');
for(var i=0; i!=fragment.length; i++) {
fragment[i].addEventListener('click', getAlert(i));
}

這樣一來就解決囉

way2. 用立即函式來解決
用立即函式直接執行當下的內容,並把當下的i值傳入,再將i值設給相對的元件

1
2
3
4
5
6
7
8
let fragment = document.querySelectorAll('div');
for(var i=0; i!=fragment.length; i++) {
(function(x){
fragment[i].addEventListener('click', function(){
alert(x);
})
})(i)
}

way3. 用let來解決
這個最快,因為,let的作用域範圍是用花括號來界定。

1
2
3
4
5
6
let fragment = document.querySelectorAll('div');
for(let i=0; i!=fragment.length; i++) {
fragment[i].addEventListener('click', function(){
alert(i);
});
}

閉包誤區

1
2
3
4
5
6
7
8
9
10
11
function count() {
var count = 0;
return function(){
count+=1;
return count;
}
}
console.log(count); // 回傳整個count函式的內容
console.log(count()); // 回傳在count函式裡面的函式的內容
console.log(count()()); // 1
console.log(count()()); // 1

上面這個範例除了要注意,呼叫count裡面的那個函式時,要再加一個小括號,才呼叫的到他喔。
另外,我們並沒有特別用一個變數來儲存count作用域中的環境,所以,當count()()執行完之後,該函式會直接被釋放掉,
並不會被保存喔,不要跟閉包的使用狀況搞混囉~

參考文章

  1. https://blog.huli.tw/2018/12/08/javascript-closure/
  2. https://www.fooish.com/javascript/function-closure.html
  3. 008天重新認識JavaScript