內(nèi)存泄漏(Memory Leak)是 Web 開發(fā)中常見但容易忽視的問題。
隨著項(xiàng)目體量增長或長時(shí)間運(yùn)行的單頁應(yīng)用增多,內(nèi)存泄漏所帶來的性能下降、頁面卡頓甚至崩潰問題變得尤為突出。
本文將帶你深入理解內(nèi)存泄漏的成因,常見場(chǎng)景,以及應(yīng)對(duì)方案,助你寫出更加穩(wěn)健高效的前端應(yīng)用。
一、什么是內(nèi)存泄漏?
1、內(nèi)存泄漏
2、GC(垃圾回收)機(jī)制是什么?
現(xiàn)代 JavaScript 引擎如 V8 采用 “可達(dá)性算法(Reachability)”。
當(dāng)某個(gè)對(duì)象從根對(duì)象(如 window、全局變量、閉包引用等)開始無法被訪問到,它就會(huì)被回收。
有些情況下會(huì)繞過垃圾回收機(jī)制,那些不需要但是不會(huì)被回收的變量等就會(huì)一直保存在內(nèi)存中,會(huì)持續(xù)浪費(fèi)內(nèi)存。
直到進(jìn)程被殺死才會(huì)徹底清除,這就是內(nèi)存泄漏產(chǎn)生的原因。
二、常見內(nèi)存泄漏場(chǎng)景
1、意外的全局變量
函數(shù)內(nèi)如果不使用 let / var / const 聲明變量,就會(huì)直接掛載到 window 對(duì)象上。
所以需要盡量避免直接寫變量,或者開始‘ use strict ’嚴(yán)格模式,避免隱式全局變量。
盡量不要使用 var 定義變量。
在全局作用域里面(即函數(shù)外)通過 var 定義的變量均會(huì)掛載在 window 對(duì)象上,會(huì)持續(xù)造成內(nèi)存泄漏。
盡量使用 const / let 來定義變量。
哪怕在全局作用域里面通過 const / let 定義的變量,也只會(huì)掛載在 Script 作用域中。
即全局詞法環(huán)境(Lexical Environment) 這是一種抽象的作用域管理方式,是瀏覽器自己維護(hù)的,所以無法訪問。
但 JavaScript 引擎知道該怎么找,相比于直接通過 var 掛載到window對(duì)象上,相對(duì)更好被GC清除一些。
function foo() {
leaked = "I am global!";
}
foo(); // leaked 被隱式掛載在 window 上
2、被遺忘的定時(shí)器/回調(diào)
閉包中的 userData 永遠(yuǎn)被回調(diào)引用,永遠(yuǎn)不被釋放。
const userData = { name: "Tom" };
setInterval(() => {
console.log(userData.name); // 閉包引用 userData
}, 1000);
所以應(yīng)該在組件銷毀時(shí)清除定時(shí)器。
const timer = setInterval(...);
clearInterval(timer);
3、閉包未釋放大對(duì)象
閉包允許函數(shù)訪問其創(chuàng)建時(shí)的作用域鏈中的變量,即使這些變量在函數(shù)外部已經(jīng)不可訪問。
閉包會(huì)保留對(duì) createClosure 函數(shù)作用域鏈的引用,這意味著 bigData 也會(huì)被保留。
function createClosure() {
const bigData = new Array(1e6);
return function () {
console.log('I am using closure');
};
}
const closure = createClosure(); // bigData 被保留
解決方案一般有兩種思路,避免大對(duì)象進(jìn)入閉包邏輯:
function createClosure() {
const bigData = new Array(1e6); // 創(chuàng)建一個(gè)大數(shù)組
console.log('I am using closure');
}
createClosure();
或者手動(dòng)釋放引用:
function createClosure() {
const bigData = new Array(1e6); // 創(chuàng)建一個(gè)大數(shù)組
return function () {
console.log('I am using closure');
bigData = null; // 手動(dòng)釋放引用
};
}
const closure = createClosure();
closure(); // 調(diào)用閉包,釋放 bigData
4、DOM 引用未解綁
DOM 被移除時(shí),如果事件監(jiān)聽沒有解綁,閉包可能繼續(xù)引用 DOM。
const element = document.getElementById('btn');
element.onclick = () => {
console.log('clicked');
};
應(yīng)在DOM被銷毀時(shí)及時(shí)解綁事件。
element.removeEventListener('click', handler);
5、被遺忘的監(jiān)聽器、websocket、觀察者等(如 IntersectionObserver)
比如元素刪除了,observer 還在監(jiān)聽,需要使用 observer.disconnect() 手動(dòng)解除觀察。
const observer = new IntersectionObserver(() => {
console.log('intersected');
});
observer.observe(document.getElementById('foo'));
6、緩存未清理(Map / WeakMap 使用不當(dāng))
Map 會(huì)強(qiáng)引用 key 和 value,導(dǎo)致無法釋放。致無法釋放。
const cache = new Map();
function addToCache(key, value) {
cache.set(key, value); // 永久引用
}
結(jié)語
說實(shí)話閉包這個(gè)東西,說簡(jiǎn)單幾句話就說完了, 但是要扯到各種作用域,垃圾回收機(jī)制,各種作用域創(chuàng)建的函數(shù)上下文,可能一萬字都講不清楚。
內(nèi)存泄漏的本質(zhì)是代碼“留下來不該留下來的東西”的體現(xiàn)。通過了解 JavaScript 的垃圾回收機(jī)制和閉包原理,可以更有意識(shí)地優(yōu)化代碼,避免性能隱患。
閱讀原文:原文鏈接
該文章在 2025/9/6 10:31:40 編輯過