閉包是什麼?在實作中我們常常會碰到閉包,或是不經意的使用閉包。至於閉包的特性與背後的機制相信許多人跟ㄚ建一樣沒有特別去了解,今天跟著我一起從下面的幾個問題來探討閉包吧~
- 閉包是什麼?
- 為何要用閉包?
- 如何使用閉包?
JavaScript的作用域與Closure(閉包)的關係密不可分,如果對於作用域(Scope)還不熟悉可以先看到這篇文章複習一下。
閉包是什麼?
先看看MDN的描述:
MDN-Closure
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
閉包(Closure)是資料結構的一種。當一個函式被宣告時,函式對其周圍狀態(詞法環境,lexical environment)的引用在一起(也可以想成函式記住了宣告時的作用域環境),這樣的組合就是閉包。
在JavaScript中,每當你建立一個函式,閉包就會在函式建立時被創建,也就是說JavaScript的函式都是閉包。
讓我們用例子來看閉包:
這邊設計了一個手槍的函式,手槍的子彈有6發,每次射擊子彈都會減少。
let bullet = 6;
function wheelGun() {
bullet--;
return bullet > 0 ? console.log("剩餘子彈:", bullet) : console.log("沒子彈了!");
}
wheelGun(); // 剩餘子彈: 5
wheelGun(); // 剩餘子彈: 4
如果這把手槍只有一把問題就不大,但如果今天我有很多把手槍時該怎麼辦?
這時為了需要分別記錄每把手槍的剩餘子彈,於是修改了程式:
// wheelGun_1
let bullet_1 = 6;
function wheelGun_1() {
bullet_1--;
return bullet_1 > 0 ? console.log("wheelGun_1剩餘子彈:", bullet_1) : console.log("wheelGun_1沒子彈了!");
}
wheelGun_1(); // 剩餘子彈: 5
wheelGun_1(); // 剩餘子彈: 4
// wheelGun_2
let bullet_2 = 10;
function wheelGun_2() {
bullet_2--;
return bullet_2 > 0 ? console.log("wheelGun_2剩餘子彈:", bullet_2) : console.log("wheelGun_2沒子彈了!");
}
wheelGun_2(); // 剩餘子彈: 9
wheelGun_2(); // 剩餘子彈: 8
wheelGun_2(); // 剩餘子彈: 7
// 其他手槍...
上面的作法無法有效管理各別手槍的使用情況,而且將子彈放在全域裡也可能讓子彈被覆蓋,導致出錯的情況。
這時就可以透過閉包把手槍函式包起來,結果如下:
function wheelGun() {
let bullet = 6;
return function shoot() {
bullet--;
return bullet > 0 ? console.log("剩餘子彈:", bullet) : console.log("沒子彈了!");
}
}
// wheelGun_1
const wheelGun_1 = wheelGun();
wheelGun_1(); // 剩餘子彈: 5
wheelGun_1(); // 剩餘子彈: 4
被包起來的wheelGun函式有獨立的作用域,當中的子彈被限制在當前的函式中,只能在函式裡取用。如此一來子彈(bullet)的變數只能在wheelGun裡使用,不會受到外部的干擾。
到這邊我們對閉包有了初步的概念,如果覺得不夠也可以看這部影片對閉包概念的說法。
接下來進一步看為什麼閉包可以做到這件事。
為何閉包可以這麼做?
這部份可以從以下2點來看
- 詞法作用域(Lexical Scope)
- 作用域鏈(Scope Chain)
詞法作用域(Lexical Scope)
這邊引用MDN的描述:
MDN-Closures
The word lexical refers to the fact that lexical scoping uses the location where a variable is declared within the source code to determine where that variable is available.
詞法作用域簡單來說: 變數的作用域是依據程式碼編寫時的位置決定,並不會在程式執行時動態決定。
由於詞法作用域不是這篇文章的主旨,如果想深入了解可以參考這篇的內容
作用域鏈(Scope Chain)
還記得在作用域一文中提到的作用域鏈嗎?
因為這個特性讓閉包可以獲取外部的變數,而作用域鏈會因為詞法作用域的機制,使作用域鏈是在函式被定義時決定,不是在被呼叫時決定。
讓我們由下面的例子了解這2點的影響:
var a = "outside";
function foo1() {
console.log(a);
}
function foo2() {
var a = "inside";
foo1();
}
foo2(); // outside
函式foo2的執行結果是outside。
因為詞法作用域的緣故,foo1與foo2是分別獨立的作用域,並且作用域在程式編寫時就決定好了。
所以foo1的a會透過作用域鏈向外找到全域的a,並不受到在foo2函式內部執行的影響。
由於詞法作用域的緣故,讓閉包內部的函式不受外部影響,當外部函式被引用時,其內部的變數可以正常在內部函式執行。
而內部函式則可以透過作用域鏈引用外部的變數。
接下來看看閉包在實作中閉包的應用。
閉包的實際應用
避免重複的運算
例如先前未使用閉包的手槍函式,如果要計算各種手槍的剩餘子彈,會讓程式變的十分冗長且可讀性也不好,這時就可以用閉包來優化。
因此使用閉包來解決需要大量計算且重複的執行的程式很有效!
封裝
我們可以透過閉包把函式封裝起來藉此模擬私有方法。這樣一來該函式具有一個獨立的作用域,讓函式中不想外露的變數被限制在閉包內部使用,避免影響全域環境,而且還可以只露出想要公開(public)的部分。
如下面的例子:
function walletCalculator() {
let total = 1000;
function calculate(val) {
total += val;
}
return {
income: (money) => {
calculate(money);
},
expenditure: (money) => {
calculate(-money);
},
balances: () => {
return total;
}
}
};
const counter1 = walletCalculator();
counter1.expenditure(150);
counter1.expenditure(200);
counter1.income(30);
console.log(counter1.balances()) // 680
例子中錢包的總金額total是walletCalculator的私有變數,不能隨意在外部環境取得及修改。
如果要變更錢包的金額,則可以透過walletCalculator公開的income及expenditure方法修改,需要知道錢包還是多少錢時可用balances取得錢包餘額。
另外,我們也可以讓閉包傳入外面的參數,稍微修改whellGun的例子,從函式外部傳入user的變數,這樣可以方便識別使用者手搶的剩餘子彈。
function wheelGun(user) {
let bullet = 6;
return function shoot() {
bullet--;
return bullet > 0 ? console.log(user, "剩餘子彈:", bullet) : console.log("沒子彈了!");
}
}
const user1_shoot = wheelGun("小明");
const user2_shoot = wheelGun("ㄚ建");
user1_shoot(); // 小明 剩餘子彈: 5
user1_shoot(); // 小明 剩餘子彈: 4
user1_shoot(); // 小明 剩餘子彈: 3
user2_shoot(); // ㄚ建 剩餘子彈: 5
記憶體回收(Garbage Collection)
當我們在程式中宣告變數時,這個變數會被分配到記憶體中儲存它的數值。當這個變數不再被程式使用時,將變數從占用的記憶體中釋放的機制便是記憶體回收。
在使用閉包時,我們會在閉包中建立各種運用,這些運用會占用記憶體空間。
當閉包的規模越大,其中包含的變數也愈多,其中也可能包含一些未使用的變數,這樣造成閉包使用大量記憶體,便可能引起效能問題。大多數使用情況,我們不需要過於擔心這方面的問題,因為JavaScript引擎有很好的記憶體回收機制來優化這部分的問題。
總結
總結一下這篇文章中我們獲得了什麼?
閉包是什麼?
- 閉包(Closure)是資料結構的一種。當一個函式被宣告時,函式對其周圍狀態(詞法環境,lexical environment)的引用在一起(也可以想成函式記住了宣告時的作用域環境),這樣的組合就是閉包。
- 閉包可以讓函式內部的變數不受外部環境的影響。
- 而閉包內部可以透過作用域鏈獲取外部資料。
閉包的特性
- 當一個函式內部回傳了另一個函式,便是有用到閉包的概念。
- 閉包可以讓函式內部的變數不受外部環境的影響。
- 而閉包內部可以透過作用域鏈獲取外部資料。
要如何用閉包?
- 可以把程式中需要重複執行的部分透過閉包封裝起來,進一步簡化程式。
- 可以將變數私有化,減少全域環境的命名衝突
- 閉包也可以將內部函式開放給外部使用。
- 閉包的結構屬於高消費的語法,使用不當會影響程式效能,大多數情況,不需要擔心這方面的問題,因為JavaScript引擎有很好的記憶體回收機制。
參考資料
JavaScript – The Complete Guide
MDN-Closures
深入淺出瞭解 JavaScript 閉包(closure)
从static/dynamic scope来谈JS的作用域
所有的函式都是閉包:談 JS 中的作用域與 Closure
如果對文章內容有任何問題或是錯誤,歡迎在底下留言讓我知道: )