什么是内存泄露
指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,
而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
1、意外的全局变量
JavaScript对未声明变量的处理方式:在全局对象上创建该变量的引用(即全局对象上的属性,不是变量,因为它能通过 delete
删除)。如果在浏览器中,全局对象就是window对象。
如果未声明的变量缓存大量的数据,会导致这些数据只有在窗口关闭或重新刷新页面时才能被释放。这样会造成意外的内存泄漏。
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"; }
这样不仅可读性高,而且后期维护也方便
谈到全局变量,需要注意那些用来临时存储大量数据的全局变量,确保在处理完这些数据后将其设置为null或重新赋值。
全局变量也常用来做cache,一般cache都是为了性能优化才用到的,为了性能,最好对cache的大小做个上限限制。
因为cache是不能被回收的,越高cache会导致越高的内存消耗。
2、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.dir
、 console.error
、 console.warn
等都存在类似的问题,这些细节需要特别的关注。
3、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
即可。
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。
4、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>
5、定时器
在JavaScript常用 setInterval()
来实现一些动画效果。当然也可以使用链式 setTimeout()
调用模式来实现:
setTimeout(function() { // do something. . . . setTimeout(arguments.callee, interval); }, interval);
如果在不需要 setInterval()
时,没有通过 clearInterval()
方法移除,那么 setInterval()
会不停地调用函数,直到调用 clearInterval()
或窗口关闭。
如果链式 setTimeout()
调用模式没有给出终止逻辑,也会一直运行下去。因此再不需要重复定时器时,确保对定时器进行清除,避免占用系统资源。
6、EventListener
做移动开发时,需要对不同设备尺寸做适配。如在开发组件时,有时需要考虑处理横竖屏适配问题。一般做法,在横竖屏发生变化时,需要将组件销毁后再重新生成。
而在组件中会对其进行相关事件绑定,如果在销毁组件时,没有将组件的事件解绑,在横竖屏发生变化时,就会不断地对组件进行事件绑定。这样会导致一些异常,甚至可能会导致页面崩掉。
同一个元素节点注册了多个相同的EventListener,那么重复的实例会被抛弃。
这么做不会让得EventListener被重复调用,也不需要用removeEventListener手动清除多余的EventListener,因为重复的都被自动抛弃了。
而这条规则只是针对于命名函数。对于匿名函数,浏览器会将其看做不同的EventListener,所以只要将匿名的EventListener,命名一下就可以解决问题: