閉包是指一個函數能夠「記住」它被創建時的外部環境(作用域),即使那個外部環境已經不存在了。
簡單來說,閉包就像是函數帶著一個「記憶背包」,裡面裝著它出生時能看到的變數。
讓我們從一個簡單的範例開始:
function outer() {
let name = "小明";
function inner() {
console.log(name); // 存取外層的 name
}
return inner; // 把內層函數返回
}
const myFunc = outer(); // outer 執行完,得到 inner 函數
myFunc(); // 輸出:小明
步驟解析:
閉包形成的條件:
閉包的背後是 作用域鏈(Scope Chain):
function createCounter() {
let count = 0; // 外層變數
return function() {
count++; // 內層函數改變它
console.log(count);
};
}
const counter = createCounter();
counter(); // 輸出:1
counter(); // 輸出:2
counter(); // 輸出:3
function createButton(text) {
let message = `你點了 ${text} 按鈕`;
return function() {
console.log(message);
};
}
const btn1 = createButton("確認");
const btn2 = createButton("取消");
btn1(); // 輸出:你點了 確認 按鈕
btn2(); // 輸出:你點了 取消 按鈕
每個按鈕函數都記住了自己獨特的 message,因為閉包讓它們各自帶著自己的「記憶背包」。
// 問題: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);
}
記憶體問題:閉包記住的變數不會被回收,如果不小心保留大物件,可能佔用記憶體。
function heavyClosure() {
let bigData = new Array(1000000).fill("資料");
return function() {
console.log(bigData[0]);
};
}
const func = heavyClosure(); // bigData 一直存在
解法:用完閉包後,讓它可以被回收(例如設為 null)。