常見的 JavaScript 內存泄露

什麼是內存泄露

指因爲疏忽或錯誤形成程序未能釋放已經再也不使用的內存。內存泄漏並不是指內存在物理上的消失,javascript

而是應用程序分配某段內存後,因爲設計錯誤,致使在釋放該段內存以前就失去了對該段內存的控制,從而形成了內存的浪費。html

一、意外的全局變量

JavaScript對未聲明變量的處理方式:在全局對象上建立該變量的引用(即全局對象上的屬性,不是變量,由於它能經過 delete刪除)。若是在瀏覽器中,全局對象就是window對象。java

若是未聲明的變量緩存大量的數據,會致使這些數據只有在窗口關閉或從新刷新頁面時才能被釋放。這樣會形成意外的內存泄漏。web

function foo(arg) {

   bar = "this is a hidden global variable with a large of data";

}

  

等同於:瀏覽器

function foo(arg) {

   window.bar = "this is an explicit global variable with a large of data";

}

  

另外,經過this建立意外的全局變量:緩存

function foo() {

   this.variable = "potential accidental global";

}


// 當在全局做用域中調用foo函數,此時this指向的是全局對象(window),而不是'undefined'

foo();

  

解決方法:

在JavaScript文件中添加 'use strict',開啓嚴格模式,能夠有效地避免上述問題。性能優化

function foo(arg) {

   "use strict" // 在foo函數做用域內開啓嚴格模式

   bar = "this is an explicit global variable with a large of data";// 報錯:由於bar尚未被聲明

}

  

若是須要在一個函數中使用全局變量,能夠像以下代碼所示,在window上明確聲明:閉包

function foo(arg) {

   window.bar = "this is a explicit global variable with a large of data";

}

這樣不只可讀性高,並且後期維護也方便ide

談到全局變量,須要注意那些用來臨時存儲大量數據的全局變量,確保在處理完這些數據後將其設置爲null或從新賦值。函數

全局變量也經常使用來作cache,通常cache都是爲了性能優化纔用到的,爲了性能,最好對cache的大小作個上限限制。

由於cache是不能被回收的,越高cache會致使越高的內存消耗。

 

二、console.log

console.log:向web開發控制檯打印一條消息,經常使用來在開發時調試分析。有時在開發時,須要打印一些對象信息,但發佈時卻忘記去掉 console.log語句,這可能形成內存泄露。

在傳遞給 console.log的對象是不能被垃圾回收 ♻️,由於在代碼運行以後須要在開發工具能查看對象信息。因此最好不要在生產環境中 console.log任何對象。

實例------>demos/log.html
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <title>Leaker</title>
</head>
<body>
 <input type="button" value="click">
 <script>

   !function () {
     function Leaker() {
       this.init();
     };

     Leaker.prototype = {

       init: function () {
         this.name = (Array(100000)).join('*');
         console.log("Leaking an object %o: %o", (new Date()), this);// this對象不能被回收
       },
       destroy: function () {

         // do something....

       }
     };
     document.querySelector('input').addEventListener('click', function () {
       new Leaker();
     }, false);
   }()

 </script>

</body>
</html>

這裏結合Chrome的Devtools–>Performance作一些分析,操做步驟以下:
   開啓【Performance】項的記錄
   執行一次CG,建立基準參考線
   連續單擊【click】按鈕三次,新建三個Leaker對象
   執行一次CG
   中止記錄

去掉console.log("Leaking an object %o: %o",(newDate()),this);語句。重複上述的操做步驟  

從對比分析結果可知, console.log打印的對象是不會被垃圾回收器回收的。所以最好不要在頁面中 console.log任何大對象,這樣可能會影響頁面的總體性能,特別在生產環境中。

除了 console.log外,另外還有 console.dirconsole.errorconsole.warn等都存在相似的問題,這些細節須要特別的關注。

 

三、closures(閉包)

當一個函數A返回一個內聯函數B,即便函數A執行完,函數B也能訪問函數A做用域內的變量,這就是一個閉包——————本質上閉包是將函數內部和外部鏈接起來的一座橋樑。

function foo(message) {
   function closure() {
       console.log(message)
   };
   return closure;
}

// 使用
var bar = foo("hello closure!");
bar()// 返回 'hello closure!'

  

在函數foo內建立的函數closure對象是不能被回收掉的,由於它被全局變量bar引用,處於一直可訪問狀態。經過執行 bar()能夠打印出 hello closure!。若是想釋放掉能夠將 bar=null便可。

因爲閉包會攜帶包含它的函數的做用域,所以會比其餘函數佔用更多的內存。過分使用閉包可能會致使內存佔用過多。

 

四、DOM泄露

在JavaScript中,DOM操做是很是耗時的。由於JavaScript/ECMAScript引擎獨立於渲染引擎,而DOM是位於渲染引擎,相互訪問須要消耗必定的資源。如Chrome瀏覽器中DOM位於WebCore,而JavaScript/ECMAScript位於V8中。

假如將JavaScript/ECMAScript、DOM分別想象成兩座孤島,兩島之間經過一座收費橋鏈接,過橋須要交納必定「過橋費」。JavaScript/ECMAScript每次訪問DOM時,都須要交納「過橋費」。

所以訪問DOM次數越多,費用越高,頁面性能就會受到很大影響。瞭解更多ℹ️

爲了減小DOM訪問次數,通常狀況下,當須要屢次訪問同一個DOM方法或屬性時,會將DOM引用緩存到一個局部變量中。

但若是在執行某些刪除、更新操做後,可能會忘記釋放掉代碼中對應的DOM引用,這樣會形成DOM內存泄露。

<script>
  var refA = document.getElementById('refA');
  var refB = document.getElementById('refB');
    document.body.removeChild(refA);
    
  // #refA不能GC回收,由於存在變量refA對它的引用。將其對#refA引用釋放,但仍是沒法回收#refA。
    refA = null
    
  // 還存在變量refB對#refA的間接引用(refB引用了#refB,而#refB屬於#refA)。將變量refB對#refB的引用釋放,#refA就能夠被GC回收。
    refB = null
</script>

 

五、定時器  

在JavaScript經常使用 setInterval()來實現一些動畫效果。固然也可使用鏈式 setTimeout()調用模式來實現:

setTimeout(function() {
  // do something. . . .
  setTimeout(arguments.callee, interval);
}, interval);

若是在不須要 setInterval()時,沒有經過 clearInterval()方法移除,那麼 setInterval()會不停地調用函數,直到調用 clearInterval()或窗口關閉。

若是鏈式 setTimeout()調用模式沒有給出終止邏輯,也會一直運行下去。所以再不須要重複定時器時,確保對定時器進行清除,避免佔用系統資源。 

 

六、EventListener

作移動開發時,須要對不一樣設備尺寸作適配。如在開發組件時,有時須要考慮處理橫豎屏適配問題。通常作法,在橫豎屏發生變化時,須要將組件銷燬後再從新生成。

而在組件中會對其進行相關事件綁定,若是在銷燬組件時,沒有將組件的事件解綁,在橫豎屏發生變化時,就會不斷地對組件進行事件綁定。這樣會致使一些異常,甚至可能會致使頁面崩掉。

 

同一個元素節點註冊了多個相同的EventListener,那麼重複的實例會被拋棄。

這麼作不會讓得EventListener被重複調用,也不須要用removeEventListener手動清除多餘的EventListener,由於重複的都被自動拋棄了。

而這條規則只是針對於命名函數。對於匿名函數,瀏覽器會將其看作不一樣的EventListener,因此只要將匿名的EventListener,命名一下就能夠解決問題:

相關文章
相關標籤/搜索