一、什麼是閉包?
閉包是指一個函數能夠「記住」它被創建時的外部環境(作用域),即使那個外部環境已經不存在了。
簡單來說,閉包就像是函數帶著一個「記憶背包」,裡面裝著它出生時能看到的變數。
二、閉包怎麼形成的?
讓我們從一個簡單的範例開始:
function outer() {
let name = "小明";
function inner() {
console.log(name); // 存取外層的 name
}
return inner; // 把內層函數返回
}
const myFunc = outer(); // outer 執行完,得到 inner 函數
myFunc(); // 輸出:小明
步驟解析:
- outer() 執行時,定義了 name = "小明" 和 inner 函數。
- inner 被返回並存到 myFunc 中。
- outer 執行完後,按理說 name 應該消失,但 inner 卻保留了對 name 的存取權。
- 呼叫 myFunc() 時,仍然能輸出 "小明"。
閉包形成的條件:
- 有一個內層函數(nested function)。
- 這個內層函數存取了外層函數的變數(比如 inner)。
- 內層函數被「帶到外面」使用(比如返回出去,或被外部變數保存)。
三、閉包的運作原理
閉包的背後是 作用域鏈(Scope Chain):
- 當 inner 被創建時,它記住了自己能看到的變數(name)。
- 這個記憶不是複製一份 name 的值,而是保留對 name 的「參考」(reference)。
- 所以即使外層函數結束,內層函數還是能透過參考找到這些變數。
四、閉包的實際應用
1.計數器(狀態管理)
function createCounter() {
let count = 0; // 外層變數
return function() {
count++; // 內層函數改變它
console.log(count);
};
}
const counter = createCounter();
counter(); // 輸出:1
counter(); // 輸出:2
counter(); // 輸出:3
- count 是 createCounter 裡的變數,外界無法直接存取,只能透過返回的函數操作。
- 返回的函數形成閉包,記住並操作 count,就像一個帶記憶的計數器。
2.製作按鈕範例
function createButton(text) {
let message = `你點了 ${text} 按鈕`;
return function() {
console.log(message);
};
}
const btn1 = createButton("確認");
const btn2 = createButton("取消");
btn1(); // 輸出:你點了 確認 按鈕
btn2(); // 輸出:你點了 取消 按鈕
每個按鈕函數都記住了自己獨特的 message,因為閉包讓它們各自帶著自己的「記憶背包」。
3. 修復迴圈問題
// 問題:var 是函數作用域,i 被共享,迴圈結束時 i = 3。
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 輸出:3, 3, 3
}, 1000);
}
// 閉包解法:
//每個 IIFE 創建了一個獨立的作用域,num 被「封裝」在閉包中,獨立於外層的 i。
//閉包讓每個 setTimeout 回調記住自己專屬的 num 值。
for (var i = 0; i < 3; i++) {
(function(num) {
setTimeout(function() {
console.log(num); // 輸出:0, 1, 2
}, 1000);
})(i);
}
五、閉包的優點
- 資料隱私:像計數器一樣,外界無法直接改動 count,只能透過閉包提供的函數操作。
- 狀態記憶:閉包能保存變數狀態,像按鈕範例記住每個按鈕的訊息。
- 靈活性:可以用來解決非同步或迴圈中的變數共享問題。
六、閉包的注意事項
記憶體問題:閉包記住的變數不會被回收,如果不小心保留大物件,可能佔用記憶體。
function heavyClosure() {
let bigData = new Array(1000000).fill("資料");
return function() {
console.log(bigData[0]);
};
}
const func = heavyClosure(); // bigData 一直存在
解法:用完閉包後,讓它可以被回收(例如設為 null)。