作为一门高级语言,JS并不像低级语言C/C++那样拥有对内存的完全掌控。JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。
正因为垃圾回收器的存在,许多人认为JS不用太关心内存管理的问题,但如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收)的情况。
内存的生命周期
JS环境中分配的内存一般有如下生命周期:
- 内存分配:当我们申明变量、函数、对象的时候,系统会自动为他们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
内存分配的几个例子:
// 为变量分配内存
var i = 11;
var s = "ifcode";
// 为对象分配内存
var person = {
age: 22,
name: 'ifcode'
};
// 为函数分配内存
function sum(a, b) {
return a + b;
}
垃圾回收算法
对垃圾回收算法来说,核心思想就是如何判断内存已经不再使用了。下面介绍两种常见浏览器的垃圾回收算法。
引用计数算法
熟悉C语言的同学的都明白,引用无非就是指向某一物体的指针。对不熟悉底层语言的同学来说,可简单将引用视为一个对象访问另一个对象的路径。(这里的对象是一个宽泛的概念,泛指JS环境中的实体)。
引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。如果没有其他对象指向它了,说明该对象已经不再需了。
下面来看个例子:
// 创建一个对象person,他有两个指向属性age和name的引用
var person = {
age: 22,
name: 'ifcode'
};
person.name = null; // 虽然设置为null,但因为person对象还有指向name的引用,因此name不会回收
var p = person;
person = 1; //原来的person对象被赋值为1,但因为有新引用p指向原person对象,因此它不会被回收
p = null; //原person对象已经没有引用,很快会被回收
由上面可以看出,引用计数算法是个简单有效的算法。但它却存在一个致命的问题:循环引用。如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。
function cycle() {
var o1 = {};
var o2 = {};
o1.a = o2;
o2.a = o1;
return "Cycle reference!"
}
cycle();
上面我们申明了一个cycle方程,其中包含两个相互引用的对象。在调用函数结束后,对象o1和o2实际上已离开函数范围,因此不再需要了。但根据引用计数的原则,他们之间的相互引用依然存在,因此这部分内存不会被回收,内存泄露不可避免了。
正是因为有这个严重的缺点,这个算法在现代浏览器中已经被下面要介绍的标记清除算法所取代了。但绝不可认为该问题已经不再存在了,因为还占有大量市场的IE6、IE7使用的正是这一算法。在需要照顾兼容性的时候,某些看起来非常普通的写法也可能造成意想不到的问题:
var div = document.createElement("div");
div.onclick = function() {
console.log("click");
};
上面这种JS写法再普通不过了,创建一个DOM元素并绑定一个点击事件。那么这里有什么问题呢?请注意,变量div有事件处理函数的引用,同时事件处理函数也有div的引用!(div变量可在函数内被访问)。一个循序引用出现了,按上面所讲的算法,该部分内存无可避免地泄露哦了。
现在你明白为啥前端程序员都讨厌IE6了吧?拥有超多BUG并依然占有大量市场的IE6是前端开发一生之敌!亲,没有买卖就没有杀害。为了让你身边的前端程序员活得健康一些,请今天就升级你的浏览器吧!
标记清除算法
上面说过,现代的浏览器已经不再使用引用计数算法了。现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
标记清除算法将“不再使用的对象”定义为“无法达到的对象”。简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。但反之未必成立。
根据这个概念,上面的例子可以正确被垃圾回收处理了。当div与其时间处理函数不能再从全局对象出发触及的时候,垃圾回收器就会标记并回收这两个对象。
如何写出对内存管理友好的JS代码?
如果还需要兼容老旧浏览器,那么就需要注意代码中的循环引用问题。或者直接采用保证兼容性的库来帮助优化代码。
对现代浏览器来说,唯一要注意的就是明确切断需要回收的对象与根部的联系。有时候这种联系并不明显,且因为标记清除算法的强壮性,这个问题较少出现。最常见的内存泄露一般都与DOM元素绑定有关:
email.message = document.createElement(“div”);
displayList.appendChild(email.message);
// 稍后从displayList中清除DOM元素
displayList.removeAllChildren();
div元素已经从DOM树中清除,也就是说从DOM树的根部无法触及该div元素了。但是请注意,div元素同时也绑定了email对象。所以只要email对象还存在,该div元素将一直保存在内存中。
小结
如果你的引用只包含少量JS交互,那么内存管理不会对你造成太多困扰。一旦你开始构建中大规模的SPA或是服务器和桌面端的应用,那么就应当将内存泄露提上日程了。不要满足于写出能运行的程序,也不要认为机器的升级就能解决一切。当你从初级程序员走向资深的时候,关注细节真是你能脱颖而出的优点。