前端常见内存泄漏及如何避免

时间:2024-11-21 15:10:11

4种常见的内存泄漏

1.意外的全局变量
全局变量是很难被垃圾回收器回收的。

  • 未声明的变量

    当我们在一个函数中给一个变量赋值但是却没有声明它时:

function fn(){
	a = "aaaaa";
}
  • 1
  • 2
  • 3

此时变量a相当于window对象下的变量

  • 使用this创建的变量
function fn(){
	this.a = "aaaaa";
}
  • 1
  • 2
  • 3
'
运行

这里的this的指向是window,因此此时创建的a变量也会被挂载到window对象下

避免此情况的解决方法

  • 避免设置全局变量
  • 在 JavaScript 文件头部或者函数的顶部加上 ‘use strict’, 开启严格模式, 使得this的指向为undefined, 这样就可以避免了
  • 必须使用全局变量存储大量数据时,确保用完后把它设置为null 或者重新定义。

2.被遗忘的计时器或回调函数

  • 在代码中使用定时器也有可能会造成内存泄漏:
var serverData = loadData()
setInterval(function() {
	var renderer = document.getElementById('renderer')
	if(renderer) {
		renderer.innerHTML = JSON.stringify(serverData)
	}
}, 5000)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

节点renderer引用了serverData.在节点renderer或者数据不再需要时,定时器依旧指向这些数据。所以哪怕当renderer节点被移除后,interval 仍旧存活并且垃圾回收器没办法回收,它的依赖也没办法被回收,除非终止定时器。

  • 对象观察者
var btn = document.getElementById('btn');
function onClick (element) {
    element.innerHTMl = "I'm innerHTML"
}
btn.addEventListener('click', onClick);
  • 1
  • 2
  • 3
  • 4
  • 5

对于上面观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。因为老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。

但是,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经可以正确检测和处理循环引用了。即回收节点内存时,不必非要调用 removeEventListener 了。

3.脱离DOM的引用
这种造成内存泄露的原因简单来说就是:
如果把DOM 存成字典(JSON 键值对)或者数组,此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。那么将来需要把两个引用都清除。

// 在对象中引用DOM
var elements = {
    btn: document.getElementById('btn')
}

function doSomeThing () {
    elements.btn.click();
}

function removeBtn () {
    // 将body中的btn移除, 也就是移除 DOM树中的btn
    document.body.removeChild(document.getElementById('button'));
    // 但是此时全局变量elements还是保留了对btn的引用, btn还是存在于内存中,不能被GC回收
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这种情况, 可以手动将引用给清除: = null

4.闭包
闭包的关键作用是:匿名函数能够访问父级作用域中的变量。

function fn () {
    var a = "I'm a";
    return function () {
        console.log(a);
    };
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
'
运行

因为变量a被fn()函数内的匿名函数所引用, 因此这种变量是不会被回收的。

var globalVar = null; // 全局变量
var fn = function () {
    var originVal = globalVar; // 局部变量
    var unused = function () { // 未使用的函数
        if (originVal) {
            console.log('call')
        }
    }
    globalVar = {
        longStr: new Array(1000000).join('*'),
        someThing: function () {
            console.log('someThing')
        }
    }
}
setInterval(fn, 100);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

每次调用fn函数的时候都会产生一个新的对象originVal;
变量unused是一个引用了originVal的闭包;
unused虽然未被使用, 但是它引用的originVal迫使它留在内存中, 并不会被回收.
解决办法是: 可以在fn的最底部, 将originVal设置成null.

Chrome 浏览器查看内存占用,按照以下步骤操作。

  • 在网页上右键, 点击“检查”打开控制台(Mac快捷键option+command+i);
  • 选择Performance面板;
  • 勾选Memory, 然后点击左上角的小黑点Record开始录制;
  • 点击弹窗中的Stop结束录制, 面板上就会显示这段时间的内存占用情况。

总结

常见的内存泄漏包括:

  • 意外的全局变量
  • 被遗忘的定时器或回调函数
  • 脱离DOM的引用
  • 闭包中重复创建的变量

如何避免内存泄漏

  • 注意程序逻辑,避免”死循环“之类的;
  • 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收
  • 避免创建过多的对象