前言
開始這個系列的原因,是因為雖然在程式中可能是不影響功能的小事,但卻會關係到整個大系統的流暢度,或是程式碼的簡潔度,這次要提及的是「閉包」(Closure)。
以往尚未完全理解閉包以前,我一直以為閉包只是用於把程式碼包起不外露,實際上閉包很大程度是關於「作用域」的理解與應用,可以說閉包其實是一個大坑,但如果不搞懂它,你的程式就會有很大機會出現作用域存取的Bug了,網路上其實也有很多介紹閉包的文章,但大部分都所以這篇文章會儘量簡單去說明閉包的原理和操作方法,如果有理解錯誤的地方,也希望大家不吝指教。
在最初學習JavaScript的時候,教學都會使用即時調用函式(IIFE)來包裹程式碼,目的是為了避免程式碼受污染,實際上這裡就應用了「閉包」存取作用域的能力,這通常是初學者第一次認知到閉包,但這只是閉包的冰山一角。
/* IIFE例子 */
(function(){
var a = 'alex';
console.log(a) // 'alex'
})()
console.log(a) // Uncaught ReferenceError: a is not defined
所以說了這麼久,閉包到底是什麼呢?
閉包定義
實際上,閉包是一種特定的資料結構,是JavaScript函式的一種固有特性,它會決定函式在呼叫時能夠存取的變數範疇,以及在記憶體保留已產生的變數。
閉包與作用域
作用域是程式語言很重要的一個概念,它決定程式中能夠取得的變數範疇,學習過JavaScript都會知道在ES6以前,能夠產生獨立作用域的就只有函式,而閉包讓作用域的變數儲存起來,不被釋放。 假設全域為國家,房子的一家之主是父親,客廳有兩張沙發;而在我則是我房間的主人,有四個玩具在裡面,實現的程式碼為:
var master = 'president';
function house() {
var master = 'father';
var sofa = 2;
(function room() {
var master = 'me';
var toys = 4;
console.log(master); // 'me'
console.log(toys); // 4
console.log(sofa); // 2
})()
console.log(master); // 'father'
console.log(toys); // Uncaught ReferenceError
console.log(sofa); // 2
})()
console.log(master); // 'president'
console.log(toys); // Uncaught ReferenceError
console.log(sofa); // Uncaught ReferenceError
雖然實際上程式碼跑到第二個console.log(toys);
時就會報錯,但我已把每個console.log()
出現的結果放到註解中,我們可以根據結果作出如下解釋:
無論在全域,house
、或是room
中,都各自宣告了master
這個變數,而在不同區域下,master
都有獨立的作用域,所以閉包第一個特性,便是確保上層的變數不被污染。
接下來我們看sofa
和toys
在各作用域中表現,無論在house
和room
中,我們都可以取得sofa
的數量,然而room
中並沒有宣告sofa
這個變數,由此我們可以知道在room
中取得的sofa
變數,是往上層的作用域進行查找的;然而當從house
想要取得toys
變數時,則會出現錯誤,這便是利用上閉包第二個特性,閉包能夠獨立儲存其中的變數不被外部修改。
閉包與回調函式
閉包經常會應用於回調函式(callback)上,確保變數能夠在函式呼叫後保留,看以下例子:
function hello(msg){
setTimeout(function() {
console.log(`hello ${msg}`)
}, 1000)
}
hello('world'); // hello world
閉包的特性讓setTimeout()
執行時,原本傳入的變數hello
函式中變數能夠一併保存起來,讓回調函式能順利運作。
閉包避免產生的誤會
通常提及閉包的重要性,都會使用迴圈配合說明,例如我們想要每隔一秒印出該秒數:
for(var i=1; i<=5; i++){
setTimeout(function(){
console.log(i);
}, i * 1000);
}
以上的程式碼結果,只會會部印出6,看起來程式碼沒有問題啊?實際上這個迴圈產生的效果是:
var i = 1;
setTimeout(function(){
console.log(i);
}, 1 * 1000);
setTimeout(function(){
console.log(i);
}, 2 * 1000);
//3,4,5,6...
i = 6;
沒有了閉包的作用域,變數i沒有儲存到setTimeout中,要謹記的是,閉包的效果需要被呼叫時才會出現,故上述的寫法並不會把i當前的值記錄下來,要把其修改為閉包的寫法,可以用函式進行包裹:
function count(i){
setTimeout(function(){
console.log(i);
}, i * 1000);
}
for(var i=1; i<=5; i++){
count(i);
}
我們加入一個count
函式,每一次迴圈,i
的值便會儲存至呼叫的count
函式中,這樣便可以輸出我們想要的結果。
為什麼要使用閉包?
閉包有許多優點,光用說明可能不好解釋,所以我們使用閉包的方式來實現場景。假設我的保險庫有10塊,桌上有5塊,我想把身上的錢存入銀行時,不會影響到桌上的金額數目(我們假設全域為桌子),實現的程式碼為:
var money = 5; // 桌上的錢
function locker(){
var money = 10; // 保險庫的錢
//存錢到保險庫的方法,以及顯示保險庫金額的方式,只有保險庫才能提供
return {
saveMoney: function (){ money +=1; console.log(money) } ,
deposit: function () { return money; }
}
}
var myLocker = locker();
myLocker.saveMoney(); // 11
console.log(money); // 5
money += 1;
console.log(money); // 6
console.log(myLocker.deposit()); // 11
在輸出端可以看到,由於我存錢到保險庫時,只會影響到保險庫內的金額,桌上的金額依然會維持;而把桌上金額增加時,也不會影響到保險庫中的存款。這便是應用上閉包的實際例子,在這個例子我們不難看出閉包的好處,閉包可以保護我們不允許被篡改的資料,除非使用閉包中提供的方法(如上述的locker中的money);另外,也可以保障全域中變數不被污染。
總結
- 閉包是一種資料結構,也是JavaScript函式特性
- 閉包能夠保存當中的能取得之變數,並且要在呼叫時才開始作用
- 閉包可以保護存入的變數不被外部修改
當然閉包並不止如此,這篇文章只能算是瞭解閉包的皮毛,要更深入瞭解,可以查看參考
參考
https://blog.techbridge.cc/2018/12/08/javascript-closure/ https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/closure.html