常见的 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,命名一下就能够解决问题:

相关文章
相关标签/搜索