Js内存泄漏详解

Js内存泄漏的几种情况与解决方法。

意外的全局变量

JavaScript 对未声明变量的处理方式:在全局对象上创建该变量的引用(即全局对象上的属性,不是变量,因为它能通过delete删除)。如果在浏览器中,全局对象就是window对象。

如果未声明的变量缓存大量的数据,会导致这些数据只有在窗口关闭或重新刷新页面时才能被释放。这样会造成意外的内存泄漏。

1
2
3
function foo(arg) {
bar = "this is a hidden global variable with a large of data";
}

等同于:

1
2
3
function foo(arg) {
window.bar = "this is an explicit global variable with a large of data";
}

另外,通过this创建意外的全局变量:

1
2
3
4
5
6
function foo() {
this.variable = "potential accidental global";
}
// 当在全局作用域中调用 foo 函数,此时 this 指向的是全局对象(window),而不是'undefined'
foo();

解决方法:

在 JavaScript 文件中添加’use strict’,开启严格模式,可以有效地避免上述问题。

1
2
3
4
function foo(arg) {
"use strict" // 在 foo 函数作用域内开启严格模式
bar = "this is an explicit global variable with a large of data";// 报错:因为 bar 还没有被声明
}

console.log

console.log:向 web 开发控制台打印一条消息,常用来在开发时调试分析。有时在开发时,需要打印一些对象信息,但发布时却忘记去掉console.log语句,这可能造成内存泄露。

在传递给console.log的对象是不能被垃圾回收 ♻️,因为在代码运行之后需要在开发工具能查看对象信息。所以最好不要在生产环境中console.log任何对象。

closures(闭包)

当一个函数 A 返回一个内联函数 B,即使函数 A 执行完,函数 B 也能访问函数 A 作用域内的变量,这就是一个闭包——本质上闭包是将函数内部和外部连接起来的一座桥梁。

1
2
3
4
5
6
7
8
9
10
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 内存泄露。如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!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>Dom-Leakage</title>
</head>
<body>
<input type="button" value="remove" class="remove">
<input type="button" value="add" class="add">
<div class="container">
<pre class="wrapper"></pre>
</div>
<script>
// 因为要多次用到<pre>节点,将其缓存到本地变量 wrapper 中,
var wrapper = document.querySelector('.wrapper');
var counter = 0;
document.querySelector('.remove').addEventListener('click', function () {
document.querySelector('.container').removeChild(wrapper);
}, false);
document.querySelector('.add').addEventListener('click', function () {
wrapper.appendChild(document.createTextNode('\t' + ++counter + ':a new line text\n'));
}, false);
</script>
</body>
</html>

导致整个pre元素和新增节点无法别回收的原因是:代码中存在全局变量wrapper对pre元素的引用。
解决方法:在删除逻辑中释放全局变量,增加wrapper = null;语句。

timers

很明显,当setInterval后没有清除定时器将会导致函数不断重复运行。

EventListener

做移动开发时,需要对不同设备尺寸做适配。如在开发组件时,有时需要考虑处理横竖屏适配问题。一般做法,在横竖屏发生变化时,需要将组件销毁后再重新生成。而在组件中会对其进行相关事件绑定,如果在销毁组件时,没有将组件的事件解绑,在横竖屏发生变化时,就会不断地对组件进行事件绑定。这样会导致一些异常,甚至可能会导致页面崩掉。

同一个元素节点注册了多个相同的 EventListener,那么重复的实例会被抛弃。这么做不会让得 EventListener 被重复调用,也不需要用 removeEventListener 手动清除多余的 EventListener,因为重复的都被自动抛弃了。而这条规则只是针对于命名函数。对于匿名函数,浏览器会将其看做不同的 EventListener,所以只要将匿名的 EventListener,命名一下就可以解决问题