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