閉包是是純函式語言的一個特性,也是 JS 的一個關鍵性的特色,雖然不了解也能開發程式,但我們不是這種人對吧?
閉包不僅可以減少某些高階功能的程式碼數量和複雜度,並且可以讓我們做到原本無法做的複雜功能。聽到這還不想認識他嗎!
那什麼是閉包呢?它是一種資料結構,可以說是一種技術,能記住函式及函式被建立時當下環境,也就是說函式可以存取在建立時作用範圍內的變數。
我們先來看一個最簡單的範例:
var outer = "global";
function outerFun(){
console.log(outer);
}
outerFun();//"global"
你可能會疑問這是閉包嗎?平常就是這樣用啊!
我們在同一個作用範圍(也就是全域)中宣告一個變數 outer
和函式 outerFun
,並呼叫函式。
這個範圍就是一個閉包,只是在程式結束時永遠不會消失,讓你沒有感覺而已。
在 JS 中每當函式被建立時,一個閉包就會產生。雖然很多人經常拿 巢狀函式來作為範例來說明閉包,但並不是只有它才能產生,要記住這點!
雖然這麼說,但我們還是會以巢狀函式來舉例,感覺有點打自己臉....
function outerFun(x){
function innerFun(){
x++
console.log(x);
}
return innerFun;
}
var num = outerFun(1);
num();//2
num();//3
在執行後會發現我的 x
怎麼每一次執行都會在加一呢?就是因為閉包造成的。
它們在函式建立時為函式及作用範圍內的變數建立了一個類似「安全氣泡」的東西,讓我們在執行時可以使用。
剛剛的程式碼,還可以簡短成這樣:
function outerFun(x){
return ()=>{
x++;
console.log(x);
}
}
var num = outerFun(1);
num();//2
num();//3
不過這邊有幾點要先各位說明,避免產生誤會:
outerFun
函式呼叫後,num
才是 函式建立,這時候 num
的閉包結構才會產生我們也可以透過下中斷點的方式觀察閉包中的值:
所以我到底可以用哪些外部變數呢?基本上一個內部函式可以有三個作用域:
內部函式可以看到(或存取)外部函式,而形成一個Scope Chain(作用域連鎖)。
這邊我們來舉一個實際的例子:
我們先建立兩個按鈕和一個顯示區
<button id="btn">Click one</button>
<button id="btn2">Click two</button>
<div id="display"></div>
然後寫一個計數器的函式,並掛上監聽事件
var btn = document.getElementById('btn');
var btn2 = document.getElementById('btn2');
var display = document.getElementById('display');
function recordClick (name){
let count = 0;
return ()=>{
count++;
console.log(count);
display.innerHTML = `${name} count : ${count}`;
}
}
btn.addEventListener("click",recordClick("Click one"));
btn2.addEventListener("click",recordClick("Click two"));
執行後可以發現到兩個按鈕並不會有共用一個 count
問題,這是因為利用閉包產生了一個類似私有變數的功能。
這邊附上 codepen範例 給大家玩玩。
那這邊可能大家會有一個問題,到底閉包是複製了這些值還是只是參照而已呢?
答案是參照,最常拿來解說的就是利用 非同步回呼函式來舉例:
function counter() {
let i = 0;
for (i = 0; i< 5; i++) {
setTimeout(function() {
console.log('counter is ' + i)
}, 1000);
}
}
counter();
可能很多人認為說結果會是 1,2,3,4,5
但是實際印出來怎麼都是 5
?
這是因為「閉包結構中所記憶的環境值是用參照指向的」, setTimeout
會先到佇列中準備延遲執行,等到回來主程式時,迴圈早就執行完了,i
也已經變成了 5
,要解決這個問題其實可以這樣解:
function timer(index){
return setTimeout(function() {
console.log('counter is ' + index)
}, 1000);
}
function counter(){
let i = 0;
for (i = 0; i< 5; i++) {
timer(i);
}
}
counter();
//counter is 0
//counter is 1
//counter is 2
//counter is 3
//counter is 4
創建一個函式 timer
讓迴圈中的 i
值傳入並形成一個閉包,因為每次傳入 timer
的值都不同,所以每個 setTimeout
閉包中的值也會不同。
我們也可以這樣解:
function showIndex(index){
return ()=> console.log(index);
}
function counter(){
let i = 0;
for (i = 0; i< 5; i++) {
setTimeout(showIndex(i), 1000);
}
}
counter();
那麼,以上就是閉包的用法,一樣如果有錯誤及來源未附上也歡迎留言指正,那麼我們明天見。
參考資料 :
你所不知道的 JS 非同步處理與效能
從ES6開始的JavaScript學習生活
忍者 JavaScript 開發技巧探秘