1、javascript中常见的内存泄漏问题
常见的内存泄漏,第一种情况,大多数由于IE浏览器无法正常关闭导致的内存占用问题;
第二种情况,是即使IE浏览器关闭,也无法释放内存。
(1)给DOM对象添加的属性是一个对象的引用。范例:
var MyObject = {};
document.getElementById('myDiv').myProp = MyObject;
解决方法:
在window.onunload事件中写上: document.getElementById('myDiv').myProp = null;
(2)DOM对象与JS对象相互引用。范例:
function Encapsulator(element) {
this.elementReference = element;
element.myProp = this;
}
new Encapsulator(document.getElementById('myDiv'));
解决方法:
在onunload事件中写上: document.getElementById('myDiv').myProp = null;
(3)给DOM对象用attachEvent绑定事件。范例:
function doClick() {}
element.attachEvent("onclick", doClick);
解决方法:
在onunload事件中写上: element.detachEvent('onclick', doClick);
(4)从外到内执行appendChild。这时即使调用removeChild也无法释放。范例:
var parentDiv = document.createElement("div");
var childDiv = document.createElement("div");
document.body.appendChild(parentDiv);
parentDiv.appendChild(childDiv);
从内到外执行appendChild:
var parentDiv = document.createElement("div");
var childDiv = document.createElement("div");
parentDiv.appendChild(childDiv);
document.body.appendChild(parentDiv);
(5)反复重写同一个属性会造成内存大量占用(但关闭IE后内存会被释放)。范例:
for(i = 0; i < 5000; i++) {
hostElement.text = "asdfasdfasdf";
}
这种方式相当于定义了5000个属性!
解决方法:
其实没什么解决方法:就是编程的时候尽量避免出现这种情况咯~~
2、内存泄漏的几种方式
(1)循环引用
循环引用基本上是所有泄漏的始作俑者。通常情况下,脚本引擎通过垃圾收集器(GC)来处理循环引 用,但是某些未知因数可能会妨碍从其环境中释放资源。对于IE来说,某些DOM对象实例的状态是脚本无法得 知的。下面是它们的基本原则:
Figure 1: 基本的循环引用模型
本模型中引起的泄漏问题基于COM的引用计数。脚本引擎对象会维持对DOM对象的引用,并在清理和释放DOM对象指针前等待所有引用的移除 。在我们的示例中,我们的脚本引擎对象上有两个引用:脚本引擎作用域和DOM对象的expando属性。当终止脚本引擎时第一个引用会释放,DOM对象引用由于在等待脚本擎的释放而并不会被释放。
(2)闭包函数(Closures)
由于闭包函数会使程序员在不知不觉中创建出循环引用,所以它对资源泄漏常常有着不可推卸的责任。而在闭包函数自己被释放前,我们很难判断父函数的参数以及它的局部变量是否能被释放。实际上闭包函数的使用已经很普通,以致人们频繁的遇到这类问题时我们却束手无策。在详细了解了闭包背后的问题和一些特殊的闭包泄漏示例后,我们将结合循环引用的图示找到闭包的所在,并找出这些不受欢迎的引用来至何处。
Figure 2. 闭包函数引起的循环引用
普通的循环引用,是两个不可探知的对象相互引用造成的,但是闭包却不同。代替直接造成引用,闭包函数则取而代之从其父函数作用域中引入信息。通常,函数的局部变量和参数只能在该被调函数自身的生命周期里使用。当存在闭包函数后,这些变量和参数的引用会和闭包函数一起存在,但由于闭包函数可以超越其父函数的生命周期而存在,所以父函数中的局部变量和参数也仍然能被访问。如果你把闭包函数作为了一个expando属性,那么你也需要通过置null将其清除。
(3)页面交叉泄漏(Cross-Page Leaks)
这种基于插入顺序而常常引起的泄漏问题,主要是由于对象创建过程中的临时对象未能被及时清理和释放造成的。它一般在动态创建页面元素,并将其添加到页面DOM中时发生。一个最简单的示例场景是我们动态创建两个对象,并创建一个子元素和父元素间的临时域(译者注:这里的域(Scope)应该是指管理元素之间层次结构关系的对象)。然后,当你将这两个父子结构元素构成的的树添加到页面DOM树中时,这两个元素将会继承页面DOM中的层次管理域对象,并泄漏之前创建的那个临时域对象。下面的图示示例了两种动态创建并添加元素到页面DOM中的方法。在第一种方法中,我们将每个子元素添加到它的直接父元素中,最后再将创建好的整棵子树添加到页面DOM中。当一些相关条件合适时,这种方法将会由于临时对象问题引起泄漏。在第二种方法中,我们自顶向下创建动态元素,并使它们被创建后立即加入到页面DOM结构中去。由于每个被加入的元素继承了页面DOM中的结构域对象,我们不需要创建任何的临时域。这是避免潜在内存泄漏发生的好方法。
Figure 3. DOM插入顺序泄漏模型
接下来,我们将给出一个躲避了大多数泄漏检测算法的泄漏示例。因为我们实际上没有泄漏任何可见的元素,并且由于被泄漏的对象太小从而你可能根本不会注意这个问题。为了使我们的示例产生泄漏,在动态创建的元素结构中将不得不内联的包含一个脚本函数指针。在我们设置好这些元素间的相互隶属关系后这将会使我们泄漏内部临时脚本对象。由于这个泄漏很小,我们不得不将示例执行成千上万次。事实上,一个对象的泄漏只有很少的字节。在运行示例并将浏览器导航到一个空白页面,你将会看到两个版本代码在内存使用上的区别。当我们使用第一种方法,将子元素加入其父元素再将构成的子树加入页面DOM,我们的内存使用量会有微小的上升。这就是一个交叉导航泄漏,只有当我们重新启动IE进程这些泄漏的内存才会被释放。如果你使用第二种方法将父元素加入页面DOM再将子元素加入其父元素中,同样运行若干次后,你的内存使用量将不会再上升,这时你会发现你已经修复了交叉导航泄漏的问题。
<html>
<head>
<script language="JScript">
function LeakMemory()
{
var hostElement = document.getElementById("hostElement");
// Do it a lot, look at Task Manager for memory response
for(i = 0; i < 5000; i++)
{
var parentDiv =
document.createElement("<div onClick='foo()'>");
var childDiv =
document.createElement("<div onClick='foo()'>");
// This will leak a temporary object
parentDiv.appendChild(childDiv);
hostElement.appendChild(parentDiv);
hostElement.removeChild(parentDiv);
parentDiv.removeChild(childDiv);
parentDiv = null;
childDiv = null;
}
hostElement = null;
}
function CleanMemory()
{
var hostElement = document.getElementById("hostElement");
// Do it a lot, look at Task Manager for memory response
for(i = 0; i < 5000; i++)
{
var parentDiv =
document.createElement("<div onClick='foo()'>");
var childDiv =
document.createElement("<div onClick='foo()'>");
// Changing the order is important, this won't leak
hostElement.appendChild(parentDiv);
parentDiv.appendChild(childDiv);
hostElement.removeChild(parentDiv);
parentDiv.removeChild(childDiv);
parentDiv = null;
childDiv = null;
}
hostElement = null;
}
</script>
</head>
<body>
<button οnclick="LeakMemory()">Memory Leaking Insert</button>
<button οnclick="CleanMemory()">Clean Insert</button>
<div id="hostElement"></div>
</body>
</html>
(4)貌似泄漏(Pseudo-Leaks)
在大多数时候,一些APIs的实际的行为和它们预期的行为可能会导致你错误的判断内存泄漏。貌似泄漏大多数时候总是出现在同一个页面的动态脚本操作中,而在从一个页面跳转到空白页面的时候发生是非常少见的。那你怎么能象排除页面间泄漏那样来排除这个问题,并且在新任务运行中的内存使用量是否是你所期望的。我们将使用脚本文本的重写来作为一个貌似泄漏的示例。 对象DOM插入顺序问题那样,这个问题也需要依赖创建临时对象来产生"泄漏"。对一个脚本元素对象内部的脚本文本一而再再而三的反复重写,慢慢地你将开始泄漏各种已关联到被覆盖内容中的脚本引擎对象
<html>
<head>
<script language="JScript">
function LeakMemory()
{
// Do it a lot, look at Task Manager for memory response
for(i = 0; i < 5000; i++)
{
hostElement.text = "function foo() { }";
}
}
</script>
</head>
<body>
<button οnclick="LeakMemory()">Memory Leaking Insert</button>
<script id="hostElement">function foo() { }</script>
</body>
</html>
下边重点解决上述(1)循环引用、(2)闭包问题中的内存泄漏问题,见3
3、私有化问题——解决内存泄漏
(1)块级作用域
在javascript中没有块级作用域,以案例1-1a、1-1b来说明
案例1-1a:
function box(){
for(var i=0;i<5;i++){
}
alert(i);
}
box();
案例1-1b:
function box(){
for(var i=0;i<5;i++){
}
var i; //即使重新声明,也不会改变之前的块级作用域
alert(i);
}
注意——在案例1-1b中,虽然重新再次声明了变量,但并不会对之前的声明造成影响。
但是,如果之前初始化了变量,再次初始化,就会覆盖之前初始化的值。
(2)使用块级作用域
//使用块级作用域(私有作用域)
function box(){
(function(){ //包含自我执行的匿名函数,就可以有私有作用域
for(var i=0;i<5;i++){
alert(i);
}
})();
//出了这个私有作用域,变量就会销毁
alert(i); //现在会显示错误,因为i已经不存在;
}
box();
总结:在匿名函数中使用的变量,当执行完该变量就会销毁
(3)闭包中的私有作用域
//私有作用域来表示
(function(){
//这里是全局的私有作用域;
var age=100;
alert(age);
})();
alert(age);
结果:(1)100
(2)undefined
总结:(1)用私有作用域代替全局变量,防止了全局污染(内存泄漏)
(2)私有作用域会解决全局变量占用内存的问题
(4)解析:私有属性、私有方法——
对于私有的属性和方法,在函数体外无法访问。
//私有变量
function box(){
var age=100; //私有变量
run:function(){
return '运行中...'; //私有方法;
}
}
(5)构造函数与普通函数的区别:构造函数名首字母可以大写
a、构造函数中属性、方法都是共有的。
b、构造函数中的公共接口
案例5-1:构造函数属性、方法公有
function Box(){
this.age=100; //公有的
this.run=function(){ //公有的
return '运行中...';
};
}
var box=new Box();
alert(box.age);
alert(box.run());
案例5-2:构造函数中公共接口——特权方法
作用:该公共接口,对外可见。虽然在局部函数中定义,但却可以在外部访问。
function Box(){
var age=100; //私有变量;
function run(){
return '运行中...';
}
this.publicGo=function(){ //对外可见的公共接口——特权方法
return age+run();
};
}
var box=new Box();
alert(box.publicGo());
案例5-3:通过构造函数传参
a、getUser()——获取值
b、setUser()——设置值value
//通过构造函数传参
function Box(){
var user=value; //私有变量;
this.getUser=function(){
return user;
};
this.setUser=function(value){
user=value;
}
}
var box=new Box('Lee');
alert(box.getUser()); //Lee
box.setUser('OOO');
alert(box.getUser());
(6)由于对象的方法,在多次调用时,会多次赋值,采用——静态私有作用域
所谓的静态私有,即采用原型prototype方法以实现共享、
(function(){
var user=''; //私有变量;
//function Box(){} //构造函数,由于它在函数体内,在外部无法访问
Box=function(value){ //全局,构造函数;
user=value;
};
Box.prototype.getUser=function(){
return user;
};
Box.prototype.setUser=function(){
user=value;
};
}();
var box=new Box('Lee'); //第一次实例化
alert(box.getUser());
var box2=new Box('Jack'); //第二次实例化
alert(box.getUser());
box2.setUser('OO');
alert(box.getUser());
结果——'Lee Jack OO'
(7)模块模式
由于之前采用的都是构造函数的方法实现私有变量及特权方法,
a、字面量使用的是模块方式实现的私有变量及方法
b、单例——永远实例化一次,即字面量对象声明方式
案例7-1:采用字面量方式声明对象
var box={
user:'Lee',
run:function(){
return '运行中...';
};
}
var box=function(){
var user='Lee'; //私有变量;
function run(){
return '运行中....';
}
var obj={
//return{
publicGo:function(){
return user+run();
}
};
return obj;
}();
alert(box.publicGo()); //Lee 运行中...
总结:(1)字面量对象声明,其实在设计模式中可以看做一个单例模式,
(2)所谓单例模式,就是永远保持对象的一个实例;
案例7-2:增强的模块模式,这种模式适合自定义对象,即“构造函数”
//先创建一个构造函数
function Desk(){}
//字面量方式创建一个对象;
var box=function(){
var user='Lee'; //私有变量;
function run(){
return '运行中....';
}
var desk=new Desk();
desk.publicGo=function(){
return user+run();
};
return desk;
}();
alert(box.publicGo()); //Lee 运行中