【JavaScript】4种常见的内存泄露 一、意外的全局变量 二、被遗忘的计时器,或回调函数 三、脱离DOM的引用 四、闭包 内存泄漏的识别方法 总结

未声明的变量-window全局变量

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

function Fn(){
	a = "hello";
}

此时这个全局变量a相当于是window对象下的一个变量:

function Fn(){
	window.a = "hello";
}

全局变量很难被垃圾回收器回收,所以会进行内存泄露。

使用this创建的变量

还有种情况

function Fn(){
	this.a = "hello";
}
Fn();

当然,这个this也是指向的window,因为此时创建的a变量会挂载到window下

  • 解决方式是:
  • 在JS文件头部或者函数的顶部加上严格模式 use strict 然后this的指向为undefined,这样就可以避免

如果必须使用全局变量存储大量数据,确保用完之后把它设置为null或者重新赋值定义。

二、被遗忘的计时器,或回调函数

定时器引起

当我们在代码中使用定时器也有可能会造成内存泄漏

var serverData = loadData();
setIntervalDemo = setInterval(function(){
	var renderer = document.getItemById('renderer');
	if(renderer){
		render.InnerHTML = JSON.stringify(serverData)
	}
},5000)

上面的例子中,节点rederer引用了serverData,在节点renderer或者数据不再需要时,定时器依旧指向这些数据
哪怕当renderer节点被移除过后,定时器interval依旧存活,所以垃圾回收器没办法回收,那么他的依赖也没法回收。

  • 解决方式是:终止定时器。clearInterval(setIntervalDemo)

对象观察者

还有个就是关于观察者模式的案例:

var btn - document.getElementById('btn');
function onClick(element){
	element.innerHTML = 'I`m innerHTML';
}
btn.addEventListener('click',onClick);

一旦他们不再需要(或者关联的对象变成不可达),明确的移除他们非常必要。因为他要一直阻塞在那里监听,浪费内存。

  • 解决方法:除了现在新浏览器的垃圾回收算法可以移除,建议还是把它写上为好
btn.removeEventListener('click',onClick);

三、脱离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的引用,并且还赋值给了这个字典,这个DOM元素还是在内存中,不能回收。
}
  • 解决方式:
    手动设置空指针移除:element.btn = null;

四、闭包

闭包,也就是局部变量销毁时
首先我们明确一点,闭包的关键就是匿名函数能够访问父级作用域中的变量

function fn(){
	var a = 'I am a';
	return function(){
		console.log(a);
	}
}

因为a被匿名函数所引用,所以这种变量不会被回收
如果情况复杂一点

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. 每次调用fn函数时候都会产生一个新对象originVal
  2. 变量unused是一个引用了originVal的闭包
  3. unused虽然未被使用,但是它引用的originVal迫使它留在内存中,并且不会回收
  • 解决方式:每次执行完对originVal进行销毁,也就是赋值空指针

内存泄漏的识别方法

  • Chrome浏览器的控制台Performance或Memory
  • Node提供的process.memoryUsage方法

Chrome浏览器的控制台Performance或Memory

相关参考链接
Chrome 浏览器查看内存占用,按照以下步骤操作。

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

Node提供的process.memoryUsage方法

console.log(process.memoryUsage());
// { rss: 27709440,
//  heapTotal: 5685248,
//  heapUsed: 3449392,
//  external: 8772 }

process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。
该对象包含四个字段,单位是字节,含义如下:

rss(resident set size):所有内存占用,包括指令区和堆栈。
heapTotal:"堆"占用的内存,包括用到的和没用到的。
heapUsed:用到的堆的部分。
external: V8 引擎内部的 C++ 对象占用的内存。

判断内存泄露, 是看heapUsed字段.

总结

内存泄漏包括:

  • 意外的全局变量 直接赋值或者通过this绑定到window的全局变量
  • 被遗忘的计时器或者回调函数或者监听器(相当于是局部作用域链的全局变量,因为这些被遗忘的里面要使用该变量)
  • 脱离DOM的引用
  • 闭包中重复创建的变量

避免内存泄漏:

  • 注意程序逻辑,避免死循环
  • 减少不必要的全局变量或者相对作用域链上的全局变量或者生命周期很长的对象,即时回收
  • 避免频繁创建过多的对象,用完了记得销毁哦

文章参考:FE-interview 么么哒~