面試官:講一下閉包?內(nèi)存泄露場景?循環(huán)引用為什么導(dǎo)致內(nèi)存泄露?怎么判斷是否存在循環(huán)引用?
Hello,大家好,我是 Sunday。
在最近的中小廠面試中,【閉包】的問題被很多公司提到。如果單純說閉包是比較簡單的,一句話就可以說清楚:“可以訪問其他函數(shù)作用域中變量的函數(shù),就是閉包函數(shù)”。
但是,隨后延伸的問題,如:閉包造成內(nèi)存泄漏的場景、循環(huán)引用為什么導(dǎo)致內(nèi)存泄露?怎么判斷是否存在循環(huán)引用? 等問題,很多同學(xué)回答的并不好。
因此,這篇文章就跟大家詳細(xì)的說一下關(guān)于閉包的問題,爭取可以做到讓大家看完這篇文章之后,對(duì)比閉包的問題可以順暢回答!
1. 什么是閉包
閉包是指 函數(shù)在創(chuàng)建時(shí)保留了對(duì)其定義作用域的引用,即使函數(shù)執(zhí)行在其詞法作用域之外,也能訪問該作用域中的變量。
閉包在 JavaScript 中的常見表現(xiàn)形式是:函數(shù)嵌套函數(shù),內(nèi)部函數(shù)訪問外部函數(shù)的變量。
由于 JavaScript 的函數(shù)是“第一類公民”,可以作為值返回、傳遞或保存,因此在外部函數(shù)返回后,閉包依然保留對(duì)外部變量的訪問權(quán)限。
function outerFunction() {
let counter = 0;
return function innerFunction() {
counter++;
console.log(counter);
};
}
const increment = outerFunction();
increment(); // 輸出: 1
increment(); // 輸出: 2
在上述代碼中,innerFunction 是一個(gè)閉包,它可以訪問 outerFunction 中的變量 counter,即使 outerFunction 已經(jīng)執(zhí)行完畢。
2. 閉包導(dǎo)致的內(nèi)存泄露場景
在 JS 中,閉包有時(shí)會(huì)導(dǎo)致內(nèi)存泄露,這是因?yàn)?閉包在訪問外部作用域的變量時(shí)會(huì)讓這些變量無法被垃圾回收,從而導(dǎo)致不必要的內(nèi)存占用。
2.1. 常見的內(nèi)存泄露場景
- 未清理的事件監(jiān)聽:如果事件監(jiān)聽器引用了外部作用域中的變量,且在不需要時(shí)未移除,則會(huì)導(dǎo)致閉包一直存在,無法釋放內(nèi)存。
function addEvent() {
const element = document.getElementById('button');
const someData = "Important data";
element.addEventListener('click', function() {
console.log(someData); // 閉包引用了外部變量 someData
});
}
addEvent();
// 這里如果不手動(dòng)移除事件監(jiān)聽器,則 someData 永遠(yuǎn)不會(huì)被釋放,造成內(nèi)存泄露
- 定時(shí)器未清理:在定時(shí)器的回調(diào)函數(shù)中使用了閉包,但在不再需要時(shí)未清除定時(shí)器,導(dǎo)致回調(diào)函數(shù)及其引用的外部變量無法被回收。
function createTimer() {
const largeData = new Array(10000).fill('*');
setInterval(function() {
console.log(largeData); // 定時(shí)器閉包持有 largeData 的引用
}, 1000);
}
createTimer();
// 這里如果不清除定時(shí)器,largeData 將永遠(yuǎn)無法釋放
3. 循環(huán)引用導(dǎo)致內(nèi)存泄露
循環(huán)引用是指:兩個(gè)或多個(gè)對(duì)象相互引用,從而形成一個(gè)循環(huán)結(jié)構(gòu),導(dǎo)致垃圾回收器無法回收這些對(duì)象。
3.1. 為什么循環(huán)引用會(huì)導(dǎo)致內(nèi)存泄露?
JS 的垃圾回收機(jī)制使用 標(biāo)記清除(mark-and-sweep) 算法。即:垃圾回收器會(huì)從根對(duì)象(如全局對(duì)象)出發(fā),查找所有可達(dá)對(duì)象。
若對(duì)象形成了循環(huán)引用,且不再被根對(duì)象訪問,則垃圾回收器無法將其清除,這會(huì)導(dǎo)致這些對(duì)象長期保留在內(nèi)存中,形成內(nèi)存泄露。
function createCircularReference() {
const objectA = {};
const objectB = {};
objectA.ref = objectB; // objectA 引用 objectB
objectB.ref = objectA; // objectB 引用 objectA,形成循環(huán)引用
}
createCircularReference();
// 這里 objectA 和 objectB 都無法被回收
在這個(gè)示例中,objectA 和 objectB 互相引用,形成了循環(huán)引用。如果沒有外部引用它們,按理說可以被垃圾回收,但由于相互持有的引用,導(dǎo)致它們無法被清除,形成內(nèi)存泄露。
4. 如何檢測(cè)循環(huán)引用
在項(xiàng)目中,如果出現(xiàn) 內(nèi)存泄漏 的問題,那么可以通過以下方式進(jìn)行檢查:
- 手動(dòng)檢測(cè):在代碼中通過邏輯分析或使用 console.log 輸出檢查對(duì)象的相互引用關(guān)系。
- 使用開發(fā)者工具檢測(cè):現(xiàn)代瀏覽器的開發(fā)者工具提供了內(nèi)存快照和堆分析,可以捕獲內(nèi)存快照來分析內(nèi)存的使用情況,幫助發(fā)現(xiàn)循環(huán)引用和內(nèi)存泄露。在 Chrome 開發(fā)者工具中,可以通過 Memory(內(nèi)存) 面板,使用 Heap Snapshot(堆快照)來查看對(duì)象的引用關(guān)系,并檢查是否有意外的循環(huán)引用。
圖片
- JSON.stringify 檢測(cè):嘗試使用 JSON.stringify 序列化對(duì)象,如果對(duì)象中存在循環(huán)引用,JSON.stringify 會(huì)拋出 TypeError 異常,可以用這種方式簡單檢測(cè)循環(huán)引用(注意這種方法只能用于檢測(cè)較簡單的循環(huán)引用,復(fù)雜場景需結(jié)合其他方法)。
function hasCircularReference(obj) {
try {
JSON.stringify(obj);
return false; // 無循環(huán)引用
} catch (error) {
return true; // 有循環(huán)引用
}
}
const objectA = {};
const objectB = { ref: objectA };
objectA.ref = objectB;
console.log(hasCircularReference(objectA)); // 輸出: true
- WeakMap 弱引用:使用 WeakMap 結(jié)構(gòu)管理對(duì)象引用。由于 WeakMap 的鍵是弱引用,不會(huì)影響對(duì)象的垃圾回收,可以通過 WeakMap 追蹤對(duì)象引用關(guān)系,并避免循環(huán)引用導(dǎo)致的內(nèi)存泄露。
5. 如何避免循環(huán)引用導(dǎo)致的內(nèi)存泄露
如果檢測(cè)出現(xiàn)內(nèi)存泄漏的問題,那么可以通過以下方式嘗試解決:
- 避免對(duì)象互相引用:在設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)時(shí),盡量避免互相引用,尤其是大的復(fù)雜對(duì)象。
- 使用 WeakMap 或 WeakSet:在 JavaScript 中,WeakMap 和 WeakSet 是弱引用結(jié)構(gòu),存儲(chǔ)在 WeakMap 或 WeakSet 中的對(duì)象不會(huì)被阻止垃圾回收??梢允褂?WeakMap 和 WeakSet 來存儲(chǔ)對(duì)象之間的引用關(guān)系,避免循環(huán)引用導(dǎo)致的內(nèi)存泄露。
const weakMap = new WeakMap();
const objectA = {};
const objectB = {};
weakMap.set(objectA, objectB);
- 在不需要時(shí)手動(dòng)斷開引用:當(dāng)對(duì)象不再使用時(shí),可以手動(dòng)將引用設(shè)為 null 或 undefined,確保垃圾回收器能夠正?;厥账鼈?。
let objectA = {};
let objectB = {};
objectA.ref = objectB;
objectB.ref = objectA;
// 當(dāng)不再需要時(shí),斷開引用關(guān)系
objectA.ref = null;
objectB.ref = null;