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
























