閉包 = 函數 + 該函數定義時的詞法環境。簡單說:內部函數引用了外部函數的變數,並被外部函數以外的範圍引用時,這些變數不會隨外部函數結束而銷毀,而是被「閉包」保存起來。
const createCounter = () => {
let count = 0; // 私有變數,外部無法直接存取
return {
increment: () => ++count,
decrement: () => --count,
getValue: () => count
};
};
const counterA = createCounter();
counterA.increment(); // 1
counterA.increment(); // 2
// count 變數被閉包保護,counterA 與 counterB 狀態隔離
// 錯誤寫法:迴圈中 var 共享同一變數
for (var i = 0; i < 3; i++) {
btns[i].onclick = () => console.log(i); // 全印 3
}
// 正確寫法 1:IIFE 建立新作用域
for (var i = 0; i < 3; i++) {
((index) => {
btns[index].onclick = () => console.log(index); // 印 0,1,2
})(i);
}
// 正確寫法 2:let 區塊作用域 (ES6+)
for (let i = 0; i < 3; i++) {
btns[i].onclick = () => console.log(i); // 印 0,1,2
}
const delay = (fn, ms, ...args) => setTimeout(() => fn(...args), ms);
const greet = (name, greeting) => console.log(`${greeting}, ${name}!`);
delay(greet, 1000, 'Alice', 'Hello'); // 1秒後印出
// Curry 化:預設參數
const format = (prefix, suffix) => (str) => `${prefix}${str}${suffix}`;
const wrapBracket = format('[', ']');
wrapBracket('content'); // "[content]"
const memoize = (fn) => {
const cache = new Map(); // 閉包保存快取
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
};
const fib = memoize(n => n <= 1 ? n : fib(n-1) + fib(n-2));
雖然現代 GC (Mark-and-Sweep) 能處理循環引用,但閉包讓變數存活時間延長,若不再需要卻未釋放引用,即造成洩漏。
// 典型洩漏:DOM 事件監聽器形成閉包,保有大量資料
function attachBigData(element, hugeData) {
element.onclick = () => {
// hugeData 被閉包捕獲,即使 element 從 DOM 移除,
// 若事件監聽器未移除,hugeData 無法被 GC
process(hugeData);
};
}
// 修正:使用 WeakMap 或移除監聽器
const dataMap = new WeakMap(); // 不阻礙 GC
function attachSafe(element, hugeData) {
dataMap.set(element, hugeData);
element.onclick = () => process(dataMap.get(element));
}
// element 移除且無其他引用時,hugeData 可被回收
// 記憶體浪費:每個實例擁有獨立的 method 函數物件
class BadExample {
constructor(data) {
this.data = data;
this.method = () => console.log(this.data); // 箭頭函數建立閉包
}
}
// 優化:方法定義在 prototype 上
class GoodExample {
constructor(data) { this.data = data; }
method() { console.log(this.data); } // 共享同一函數
}
閉包是 JavaScript 函數式特性的核心,賦予了函數「記憶」出生環境的能力。理解其背後的作用域鏈機制,善用模組模式、Curry 化、記憶化等模式,並警惕非預期的變數捕獲導致記憶體洩漏,是進階前端工程師的必修課。