[JS] 深入淺出 JavaScript 閉包(closure)
雖然之前在 Udemy 上課時有整理了幾篇關於閉包的筆記,但其實當時對於閉包的概念和使用上還是不很清楚,只是大概知道有這個概念。
前陣子在 Treehouse 上課時,聽到了另一個用來說明閉包的例子,我覺得講解的非常 清楚,在這裡做個筆記記錄一下。
個人覺得先瞭解閉包的應用,接著在回去看之前上 Udemy 時整理的和閉包有關的文章時,在理解上更有幫助。
不使用閉包(closure)的情況
在 JavaScript 中,global variable 的錯用可能會使得我們的程式碼出現不可預期的錯誤。
假設我們現在要做一個計數的程式,一開始我們想要先寫一個給狗的計數函式:
// 狗的計數程式
var count = 0;
function countDogs() {
count += 1;
console.log(count + ' dog(s)');
}
countDogs(); // 1 dog(s)
countDogs(); // 2 dog(s)
countDogs(); // 3 dog(s)
接著繼續寫程式的其他部分,當寫到程式的後面時,我發現我也需要寫貓的計數程式,於是我又開始寫了貓的計數程式:
// 狗的計數函式
var count = 0;
function countDogs() {
count += 1;
console.log(count + ' dog(s)');
}
// 中間是其他程式碼...
// 貓的計數函式
var count = 0;
function countCats() {
count += 1;
console.log(count + ' cat(s)');
}
countCats(); // 1 cat(s)
countCats(); // 2 cat(s)
countCats(); // 3 cat(s)
乍看之下運作上好像沒有問題,當我執行 countDogs()
或 countCats()
,都會讓 count 增加,然而問題在於當我在不注意的情況下把 counter
這個變數建立在了全域的環境底下時,不論是執行 countDogs()
或是 countCats()
時,都是用到了全域的 count 變數,這使得當我執行下面的程式時,他沒有辦法分辨現在到底是在對狗計數還是對貓計數,進而導致把貓的數量和狗的數量交錯計算的錯誤情況:
var count = 0;
function countDogs() {
count += 1;
console.log(count + ' dog(s)');
}
// 中間是其他程式碼...
var count = 0;
function countCats() {
count += 1;
console.log(count + ' cat(s)');
}
countCats(); // 1 cat(s)
countCats(); // 2 cat(s)
countCats(); // 3 cat(s)
countDogs(); // 4 dog(s),我希望是 1 dog(s)
countDogs(); // 5 dog(s),我希望是 2 dog(s)
countCats(); // 6 cat(s),我希望是 4 cat(s)
透過閉包讓 function 能夠有 private 變數
從上面的例子我們知道,如果錯誤的使用全域變數,程式很容易會出現一些莫名其妙的 bug ,這時候我們就可以利用閉包(closure)的作法 ,讓函式有自己私有變數,簡單來說就是 countDogs 裡面能有一個計算 dogs 的 count
變數;而 countCats 裡面也能有一個計算 cats 的 count
變數,兩者是不會互相干擾的。
為了達到這樣的效果,我們就要建立閉包,讓變數保留在該函式中而不會被外在環境干擾。
改成閉包的寫法會像這樣:
function dogHouse() {
var count = 0;
function countDogs() {
count += 1;
console.log(count + ' dogs');
}
return countDogs;
}
const countDogs = dogHouse();
countDogs(); // "1 dogs"
countDogs(); // "2 dogs"
countDogs(); // "3 dogs"
這樣我們就將專門計算狗的變數 count
關閉在 dogHouse 這個函式中,上面這是閉包的基本寫法,當你看到一個 function 內 return 了另一個 function,通常就是有用到閉包的概念。
從程式碼中我們可以看到在 dogHouse
這個函式中裡面的 countDogs()
才是我們真正執行計數的函式:
而在 dogHouse
這個函式中存在 count 這個變數,由於 JavaScript 變數會被縮限在函式的執行環境中,因此這個 count
的值只有在 dogHouse
裡面才能被取用,在 dogHouse
函式外是取用不到這個值的。
最後因為我們要能夠執行在 dogHouse
中真正核心 countDogs()
這個函式,因此我們會在最後把這個函式給 return 出來,好讓我們可以在外面去呼叫到 dogHouse
裡面的這個 countDogs()
函式:
接著,當我們在使用閉包時,我們先把存在 dogHouse
裡面的 countDogs
拿出來用,並一樣命名為 countDogs
(這裡變數名稱可以自己取),因此當我執行全域中的 countDogs
時,實際上會執行的是 dogHouse
裡面的 countDogs
函式:
上面的例子就是一個很基本的閉包的寫法,一個 function 裡面包了另一個 function,同時會 return 裡面的 function 讓我們可以在外面使用到它。
我們可以把我們最一開始的程式碼都改成使用閉包的寫法:
function dogHouse() {
var count = 0;
function countDogs() {
count += 1;
console.log(count + ' dogs');
}
return countDogs;
}
function catHouse() {
var count = 0;
function countCats() {
count += 1;
console.log(count + ' cats');
}
return countCats;
}
const countDogs = dogHouse();
const countCats = catHouse();
countDogs(); // "1 dogs"
countDogs(); // "2 dogs"
countDogs(); // "3 dogs"
countCats(); // "1 cats"
countCats(); // "2 cats"
countDogs(); // "4 dogs"
當我們正確的使用閉包時,雖然一樣都是使用 count
來計數,但是是在不同執行環境內的 count
因此也不會相互干擾。
進一步瞭解和使用閉包
另外,甚至在運用的是同一個 dogHouse 時,變數間也都是獨立的執行環境不會干擾,例如:
function dogHouse() {
var count = 0;
function countDogs() {
count += 1;
console.log(count + ' dogs');
}
return countDogs;
}
// 雖然都是使用 dogHouse ,但是各是不同的執行環境
// 因此彼此的變數不會互相干擾
var countGolden = dogHouse();
var countPug = dogHouse();
var countPuppy = dogHouse();
countGolden(); // 1 dogs
countGolden(); // 2 dogs
countPug(); // 1 dogs
countPuppy(); // 1 dogs
countGolden(); // 3 dogs
countPug(); // 2 dogs
將參數代入閉包中
但是這麼做你可能覺得不夠清楚,因為都是叫做 dogs,這時候我們一樣可以把外面的變數透過函式的參數代入閉包中,像是下面這樣,回傳的結果就清楚多了:
// 透過函式的參數將值代入閉包中
function dogHouse(name) {
var count = 0;
function countDogs() {
count += 1;
console.log(count + ' ' + name);
}
return countDogs;
}
// 同樣是使用 dogHouse 但是使用不同的參數
var countGolden = dogHouse('Golden');
var countPug = dogHouse('Pug');
var countPuppy = dogHouse('Puppy');
// 結果更清楚了
countGolden(); // 1 Golden
countGolden(); // 2 Golden
countPug(); // 1 Pug
countPuppy(); // 1 Puppy
countGolden(); // 3 Golden
countPug(); // 2 Pug