- 資訊首頁(yè) > 開(kāi)發(fā)技術(shù) >
- JavaScript中怎么防范內存泄漏
這篇文章給大家介紹JavaScript中怎么防范內存泄漏,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
瀏覽器將對象保留在堆內存中,通過(guò)引用鏈可從根對象到達這些對象。垃圾回收器(GC)是 JavaScript 引擎中的一個(gè)后臺進(jìn)程,它可以識別無(wú)法到達的對象,將其刪除,并回收相應的內存。
引用鏈 - GC - 對象關(guān)系圖
當內存中本應在垃圾回收循環(huán)中被清理的對象,通過(guò)另一個(gè)對象意外的引用從而維持可訪(fǎng)問(wèn)狀態(tài),就會(huì )發(fā)生內存泄漏。將多余的對象保持在內存中,會(huì )導致應用程序內部的內存使用量過(guò)大,進(jìn)而影響性能。
內存泄露
如何判斷代碼是否存在內存泄漏呢?內存泄漏通常比較隱蔽,難以發(fā)現和定位。造成內存泄漏的 JavaScript 代碼看上去挺正常,瀏覽器在運行的時(shí)候也不會(huì )拋出錯誤。如果發(fā)現頁(yè)面性能越來(lái)越差,通常是內存泄漏的征兆,可以通過(guò)瀏覽器內置的工具判斷是否存在內存泄漏,并分析出原因。
最快的方法是查看瀏覽器的任務(wù)管理器(注意,不是操作系統的任務(wù)管理器)。它提供了瀏覽器運行中的所有 tab 頁(yè)和進(jìn)程的資源使用情況,比如內存占用、CPU 占用和進(jìn)程 ID 等。Chrome 的任務(wù)管理器可通過(guò) Shift+Esc 快捷鍵打開(kāi),Firefox 可在地址欄輸入about:performance打開(kāi)。
如果頁(yè)面都沒(méi)有任何交互,內存占用卻越來(lái)越多,很可能存在泄漏。
Chrome 任務(wù)管理器
瀏覽器 DevTools 則提供了更豐富的內存管理功能??梢栽?Chrome 的性能面板錄制頁(yè)面運行情況,查看可視化的性能分析數據。
Chrome 性能面板
除此之外,Chrome 和 Firefox 的 DevTools 還有專(zhuān)門(mén)的內存工具用于分析內存使用情況。通過(guò)比較連續的內存快照,可以看出內存分配情況。
通過(guò)前面的分析,內存泄露的根本原因就是代碼在無(wú)意之中引用了本該被 GC 回收的對象。那么,哪些情況容易造成內存泄露呢?
1、意外的全局變量
全局變量一直處于可訪(fǎng)問(wèn)狀態(tài),不會(huì )被 GC 回收。在非嚴格模式下,有時(shí)會(huì )不小心讓局部變量變成全局變量。
給未聲明的變量賦值
使用指向全局對象的 this
function createGlobalVariables() { leaking1 = '變成全局變量了'; // 給未聲明的變量賦值 this.leaking2 = '這也是全局變量'; // 'this' 指向全局對象 }; createGlobalVariables(); window.leaking1; // '變成全局變量了' window.leaking2; // '這也是全局變量'
如何避免: 嚴格模式 ("use strict") 會(huì )避免意外的全局變量,以上代碼在嚴格模式下會(huì )報錯。
2、閉包
函數作用域變量在函數執行完后會(huì )被清理,前提是在函數外部沒(méi)有引用它。閉包會(huì )讓變量一直處于被引用狀態(tài),即使它的執行上下文和作用域已經(jīng)不存在了。
function outer() { const Array = []; return function inner() { bigArray.push('Hello'); console.log('Hello'); }; }; const sayHello = outer(); // 包含了對 inner 的引用 function repeat(fn, num) { for (let i = 0; i < num; i++){ fn(); } } repeat(sayHello, 10); // 每次調用 sayHello 都會(huì )添加 'Hello' 到potentiallyHugeArray // 如果是10萬(wàn)次呢?闊怕:repeat(sayHello, 100000)
上面例子中的數組 bigArray 沒(méi)有從任何函數中直接返回,因此無(wú)法直接訪(fǎng)問(wèn),但是它卻不停地膨脹,取決于我們調用了多少次 function inner()。
如何避免: 閉包是 JavaScript 語(yǔ)言的特性之一,如果無(wú)法避開(kāi),那就請注意兩點(diǎn):
清楚閉包是何時(shí)創(chuàng )建的,以及哪些對象會(huì )被保留在內存中;
清楚閉包的生命周期和用途(尤其是當做回調函數的時(shí)候)
3、定時(shí)器
在 setTimeout 或 setInterval 的回調函數中引用某些對象,是防止被 GC 回收的常見(jiàn)做法。如果在代碼里設置循環(huán)定時(shí)器(setTimeout也能像setInterval一樣定時(shí)重復執行,只要設置成遞歸調用),只要定時(shí)器還在運行,回調函數中的對象就會(huì )一直保持在內存中。
下面的例子中,data 對象會(huì )在清除定時(shí)器后被 GC 回收。但我們沒(méi)有獲取 setInterval的返回值,也就沒(méi)辦法用代碼清除這個(gè)定時(shí)器,因此盡管完全沒(méi)有用到,data.hugeString 也會(huì )一直保留在內存中,直到進(jìn)程結束。
function setCallback() { const data = { counter: 0, hugeString: new Array(100000).join('x') }; return function cb() { data.counter++; // data 對象現在已經(jīng)屬于回調函數的作用域了 console.log(data.counter); } } setInterval(setCallback(), 1000); // 沒(méi)法停止定時(shí)器了
如何避免: 對于生命周期不確定的回調函數,我們應該:
注意被定時(shí)器回調函數引用的對象
使用定時(shí)器返回的句柄,在必要時(shí)清除它
也可以通過(guò)分離變量的方式,避免對大對象的引用:
function setCallback() { // 分開(kāi)定義變量 let counter = 0; const hugeString = new Array(100000).join('x'); // setCallback執行完即可被回收 return function cb() { counter++; // 只剩 counter 位于回調函數作用域 console.log(counter); } } const timerId = setInterval(setCallback(), 1000); // 保存定時(shí)器 ID // 執行某些操作 ... clearInterval(timerId); // 停止定時(shí)器
4、事件監聽(tīng)器
活動(dòng)的事件監聽(tīng)器會(huì )阻止作用域內的變量被 GC 回收。事件監聽(tīng)器一直處于活動(dòng)狀態(tài),直到用 removeEventListener() 顯式移除,或者關(guān)聯(lián)的 DOM 元素被移除。
對于有些事件來(lái)說(shuō),監聽(tīng)器需要一直保留,直到頁(yè)面被銷(xiāo)毀。比如按鈕點(diǎn)擊事件,我們可能需要重復使用。但是,有時(shí)候我們希望某個(gè)事件只執行特定次數。
const hugeString = new Array(100000).join('x'); document.addEventListener('keyup', function() { // 匿名監聽(tīng)器無(wú)法移除 doSomething(hugeString); // hugeString 會(huì )一直處于回調函數的作用域內 });
上面例子中的事件監聽(tīng)器用了匿名函數,這樣就沒(méi)法用removeEventListener()移除了。同時(shí),document元素也無(wú)法刪除,因此事件回調函數內的變量會(huì )一直保留,哪怕我們只想觸發(fā)一次事件。
如何避免: 事件監聽(tīng)器不再需要時(shí),要記得解除綁定。使用具名函數方式獲取引用,通過(guò)removeEventListener()解除綁定。
function listener() { doSomething(hugeString); } document.addEventListener('keyup', listener); document.removeEventListener('keyup', listener);
如果事件監聽(tīng)器只需要執行一次, addEventListener()可以接受第三個(gè)參數,是一個(gè)配置對象。指定{once: true},監聽(tīng)器函數會(huì )在事件觸發(fā)一次執行后自動(dòng)移除(匿名函數也可以)。
document.addEventListener('keyup', function listener(){ doSomething(hugeString); }, {once: true}); // 執行一次后自動(dòng)移除事件監聽(tīng)器
5、緩存
如果持續不斷地往緩存里增加數據,沒(méi)有定時(shí)清除無(wú)用的對象,也沒(méi)有限制緩存大小,那么緩存就會(huì )像滾雪球一樣越來(lái)越大。
let user_1 = { name: "Kayson", id: 12345 }; let user_2 = { name: "Jerry", id: 54321 }; const mapCache = new Map(); function cache(obj){ if (!mapCache.has(obj)){ const value = `${obj.name} has an id of ${obj.id}`; mapCache.set(obj, value); return [value, 'computed']; } return [mapCache.get(obj), 'cached']; } cache(user_1); // ['Kayson has an id of 12345', 'computed'] cache(user_1); // ['Kayson has an id of 12345', 'cached'] cache(user_2); // ['Jerry has an id of 54321', 'computed'] console.log(mapCache); // ((…) => "Kayson has an id of 12345", (…) => "Jerry has an id of 54321") user_1 = null; //Garbage Collector console.log(mapCache); // ((…) => "Kayson has an id of 12345", (…) => "Jerry has an id of 54321") // 依然在緩存里
上面的例子中,緩存依然保留了user_1 的數據。因此我們需要把不再使用的數據從緩存中刪除。
可能的解決方案: 為了解決這個(gè)問(wèn)題,可以使用 WeakMap。 WeakMap 是一種數據結構,它只用對象作為鍵,并保持對象鍵的弱引用,如果這個(gè)對象被置空了,相關(guān)的鍵值對會(huì )被 GC 自動(dòng)回收。
let user_1 = { name: "Kayson", id: 12345 }; let user_2 = { name: "Jerry", id: 54321 }; const weakMapCache = new WeakMap(); function cache(obj){ // 代碼跟前一個(gè)例子相同,只不過(guò)用的是 weakMapCache return [weakMapCache.get(obj), 'cached']; } cache(user_1); // ['Kayson has an id of 12345', 'computed'] cache(user_2); // ['Jerry has an id of 54321', 'computed'] console.log(weakMapCache); // ((…) => "Kayson has an id of 12345", (…) => "Jerry has an id of 54321"} user_1 = null; // Garbage Collector console.log(weakMapCache); // ((…) => "Jerry has an id of 54321") - 第一條記錄已被 GC 刪除
6、分離的 DOM 元素
如果 DOM 節點(diǎn)被 JavaScript 代碼直接引用,即使從 DOM 樹(shù)分離,也不會(huì )被 GC 回收。
下面的例子中,removeChild() 達不到預期效果,堆快照會(huì )顯示HTMLDivElement處于分離狀態(tài),因為有個(gè)變量指向了這個(gè)div。
function createElement() { const div = document.createElement('div'); div.id = 'detached'; return div; } // 即使調用了deleteElement() ,依然保存著(zhù) DOM 元素的引用 const detachedDiv = createElement(); document.body.appendChild(detachedDiv); function deleteElement() { document.body.removeChild(document.getElementById('detached')); } deleteElement(); // 堆快照顯示: detached div#detached
如何避免: 一種方法是把DOM 引用限制為局部作用域。
function createElement() {...} // // DOM 引用位于函數作用域內 function appendElement() { const detachedDiv = createElement(); document.body.appendChild(detachedDiv); } appendElement(); function deleteElement() { document.body.removeChild(document.getElementById('detached')); } deleteElement();
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自互聯(lián)網(wǎng)轉載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權請聯(lián)系QQ:712375056 進(jìn)行舉報,并提供相關(guān)證據,一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容。
Copyright ? 2009-2021 56dr.com. All Rights Reserved. 特網(wǎng)科技 特網(wǎng)云 版權所有 珠海市特網(wǎng)科技有限公司 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2
建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站