我以為自己懂閉包,直到遇見了它的真面目
每個 JavaScript 開發者都認為自己理解閉包,直到他們發現其實并非如此。
閉包是個看似簡單,但一不小心就會出現意外行為的概念。
我自認為已經掌握閉包,結果碰到了意想不到的問題。
接下來,我們一起好好剖析一下閉包。
閉包到底是什么?
簡單來說,閉包是函數與其聲明時所處的詞法環境(也就是外部作用域變量的引用)的結合體。
換句話說,閉包讓函數可以訪問其外層作用域中的變量,即使外層作用域已經執行結束。
閉包 = 函數 + 它所“保留”的詞法作用域。
聽起來很直觀吧?
來看個經典例子:
function outerFunction() {
let count = 0;
function innerFunction() {
console.log(count);
}
return innerFunction;
}
const fn = outerFunction();
fn(); // 0盡管 outerFunction() 已經執行完畢并返回,innerFunction() 仍能訪問 count。這就是閉包的魔力。
復雜部分:循環里的閉包陷阱
如果閉包這么簡單,大家不會頭疼。
問題出在循環里。
假設你想在循環中創建函數數組,每個函數打印它在循環中的索引。
你猜會輸出什么?
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(() => console.log(i));
}
funcs[0](); // ?
funcs[1](); // ?
funcs[2](); // ?如果你猜是 0 1 2,恭喜你,錯了。
實際輸出是:
3
3
3這是什么情況?
原因和解決方案
問題在于:
var i 是函數作用域,不是塊級作用域。當函數執行時,循環已經結束,i 的值變成了3。
解決方案一:用 let 替代 var
let 是塊級作用域,每次循環都會捕獲一個新的 i。
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(() => console.log(i));
}
funcs[0](); // 0 ?
funcs[1](); // 1 ?
funcs[2](); // 2 ?這樣問題迎刃而解。
解決方案二:使用立即執行函數表達式(IIFE)
如果只能用 var,用 IIFE 創建單獨作用域:
const funcs = [];
for (var i = 0; i < 3; i++) {
(function(index) {
funcs.push(() => console.log(index));
})(i);
}
funcs[0](); // 0 ?
funcs[1](); // 1 ?
funcs[2](); // 2 ?每個 IIFE 都“凍結”了當時的 i 值,保證正確輸出。
閉包在實際開發中的用武之地
閉包不僅是理論,更是日常開發中不可或缺的工具。
1. 數據隱私(封裝)
想要創建私有變量?閉包幫你實現。
function createCounter() {
let count = 0;
return {
increment: () => count++,
getCount: () => count
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1 ?count 不可被外部直接訪問,實現了私有狀態。
2. 記憶狀態的事件監聽器
事件監聽器需要記住狀態時,閉包非常實用。
function attachListener(buttonId) {
let clicks = 0;
document.getElementById(buttonId).addEventListener("click", () => {
clicks++;
console.log(`按鈕被點擊了 ${clicks} 次`);
});
}
attachListener("myButton");即使 attachListener() 已執行完,事件回調依舊能訪問和更新 clicks。
常見閉包誤區
- ? 誤區一:以為閉包會復制變量 閉包其實保存的是變量的引用,變量變了閉包看到的也變(這也是循環陷阱的根源)。
- ? 誤區二:閉包導致內存泄漏 如果閉包引用了大型對象,可能阻止垃圾回收,導致內存泄漏。
示例:
function createLeakyFunction() {
let hugeObject = new Array(1000000).fill("??");
return () => console.log(hugeObject.length);
}
const leaky = createLeakyFunction();
// hugeObject 仍在內存中解決辦法是及時釋放引用:
function createLeakyFunction() {
let hugeObject = new Array(1000000).fill("??");
return () => {
console.log(hugeObject.length);
hugeObject = null; // 釋放內存
};
}你真的理解閉包了嗎?
閉包是 JavaScript 中最容易被誤解的概念之一,但一旦掌握,你會在無數場景發現它的身影。
無論是狀態管理、循環問題,還是封裝邏輯,閉包都給你強大能力——前提是用得正確。
重點總結
- ?? 閉包讓函數能訪問其外層作用域的變量。
- ?? 用
var循環時要注意閉包陷阱,優先用let或 IIFE。 - ?? 閉包適合實現私有變量、事件監聽狀態保持等場景。
- ?? 留意閉包持有大對象引用,避免內存泄漏。
現在,去寫更干凈、更聰明的 JavaScript 吧!??
























