导读:
最近在网上查阅了不少Javascript闭包(closure)相关的资料,写的大多是非常的学术和专业。对于初学者来说别说理解闭包了,就连文字叙述都很难看懂。撰写此文的目的就是用最通俗的文字揭开Javascript闭包的真实面目。
一、什么是闭包?
“官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
相信很少有人能直接看懂这句话,因为他描述的太学术。我想用如何在Javascript中创建一个闭包来告诉你什么是闭包,因为跳过闭包的创建过程直接理解闭包的定义是非常困难的。看下面这段代码:
function a(){
var i=0;
function b(){
alert(++i);
}
return b;
}
var c = a();
c();
这段代码有两个特点:
1、函数b嵌套在函数a内部;
2、函数a返回函数b。
这样在执行完var c=a()后,变量c实际上是指向了函数b,再执行c()后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个闭包,为什么?因为函数a外的变量c引用了函数a内的函数b,就是说:
当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个闭包。
我猜想你一定还是不理解闭包,因为你不知道闭包有什么作用,下面让我们继续探索。
二、闭包有什么作用?
简而言之,闭包的作用就是在a执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a所占用的资源,因为a的内部函数b的执行需要依赖a中的变量。这是对闭包作用的非常直白的描述,不专业也不严谨,但大概意思就是这样,理解闭包需要循序渐进的过程。
在上面的例子中,由于闭包的存在使得函数a返回后,a中的i始终存在,这样每次执行c(),i都是自加1后alert出i的值。
那么我们来想象另一种情况,如果a返回的不是函数b,情况就完全不同了。因为a执行完后,b没有被返回给a的外界,只是被a所引用,而此时a也只会被b引用,因此函数a和b互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。(关于Javascript的垃圾回收机制将在后面详细介绍)
三、闭包内的微观世界
如果要更加深入的了解闭包以及函数a和嵌套函数b的关系,我们需要引入另外几个概念:函数的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数a从定义到执行的过程为例阐述这几个概念。
1、当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时a所在的“环境”,如果a是一个全局函数,则scope chain中只有window对象。
2、当函数a执行的时候,a会进入相应的执行环境(excution context)。
3、在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就为第1步中的scope chain。即a.scope=a的作用域链。
4、然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到a的作用域链的最顶端。此时a的作用域链包含了两个对象:a的活动对象和window对象。
5、下一步是在活动对象上添加一个arguments属性,它保存着调用函数a时所传递的参数。
6、最后把所有函数a的形参和内部的函数b的引用也添加到a的活动对象上。在这一步中,完成了函数b的的定义,因此如同第3步,函数b的作用域链被设置为b所被定义的环境,即a的作用域。
到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。
当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象,如下图所示:
如图所示,当在函数b中访问一个变量的时候,搜索顺序是先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索函数a的活动对象,依次查找,直到找到为止。如果整个作用域链上都无法找到,则返回undefined。如果函数b存在prototype原型对象,则在查找完自身的活动对象后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。
四、闭包的应用场景
1、保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。
2、在内存中维持一个变量。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。
以上两点是闭包最基本的应用场景,很多经典案例都源于此。
五、Javascript的垃圾回收机制
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。
[最后修改由 Felix, 于 2007-12-11 11:06:17]
本文转自
http://www.felixwoo.com/article.asp?id=294
javascript语言中的闭包
写在前面的话:
试图翻译自 http://jibbering.com/faq/faq_notes/closures.html
文中大量提到《ECMA 262 》,我也没时间读这东西,可能有问题理解有误。希望纠正。
只译了前边部分,我得理解几天再继续下去。
英文水平差,凑合看吧。
国内找了半天没这篇文章中文版,献丑了就。
读后有种豁然开朗的感觉,清楚了很多javascript的问题。
一、Introduction
Closure (闭包)
A "closure" is an expression (typically a function) that can have free variables together with an environment that binds those variables (that "closes" the expression).
闭包是ECMAScript(javascript)语言强大的特征之一,如果没有真正的理解它的概念,不可能很好使用它。在一般浏览器环境中,它们很容易被建立,但也会造成比较难理解的代码逻辑。为了避免闭包引起的缺点,利用它所提供的优点,明白它的机制是重要的。javascript语言的闭包很大程度上依靠 scope chains(函数,变量的范围链) 和 javascript对象的灵活的属性机制 实现。
闭包简单的解释是,ECMAScript允许inner functions(嵌套函数):函数可以定义在另外一个函数里面(关于嵌套函数可以看看<javascript权威指南>)。这些内部的函数可以访问outer function(父函数)的local变量,参数,其它内部函数。当内部函数被构造,并可以在函数外被获得(函数当成返回值),这个内部函数被在 outer function返回后被执行(在outer函数外执行),那一个闭包形成了。(简单的理解,function被当成数据类型传递或动态执行)。 inner function还有权利访问 那些outer function(父函数)的local变量,参数,其它内部函数。那些outer function(父函数)的local变量,参数,其它内部函数在outer function返回前就有值,并返回的inner function需要改变这些值。
我估计以下代码就是一个闭包。
< script >
var g_count = 0 ;
function aaa(p) { // outer function
var outer_count = 0 ;
function innerfun(name,count) { // outer function中定义的另外一个inner function
return name + ' : ' + count + ' 次; ' ;
}
return function() { // 返回一个匿名的inner函数
var inner_count = 0 ;
return innerfun( ' g_count ' ,( ++ g_count)) + innerfun( ' outer_count ' ,( ++ outer_count))+innerfun('inner_count',(++inner_count))+p;
}
}
var fun1=aaa("fun1");
var fun2=aaa("fun2");
alert(fun1)
alert(fun1());//这时候才真正执行
alert(fun2());
</script>
不幸的,完全明白闭包需要知道它背后的机制和一些技术细节。
二、The Resolution of Property Names on Objects (javascript对象的属性)
ECMAScript认可两类对象,“Native Object”和“Host Object”,Host Object属于Native Object的子类,在(ECMA 262 3rd Ed Section 4.3)中叫"Built-in Object"(内置对象)。Native objects属于语言级别,host objects被环境提供(浏览器等),例如,document objects,DOM nodes等。
关于对象属性的存取,数据类型,原型对象prototype的使用,我这就不译了。
可以参见我的另一篇文章
三、Identifier Resolution, Execution Contexts and Scope Chains
1、The Execution Context
执行环境上下文(Execution Context)是个抽象的概念,the ECMSScript specification (ECMA 262 3rd edition) to define the behaviour required of ECMAScript implementations。规范没有说 execution contexts 应该怎样实现,但规范中提到execution contexts是个关联属性结构,因此你可以假想为一个有属性的对象,但不是公有的(public)。
所有的javascript代码在一个execution context中执行。Global代码(.js文件中)在我叫做globla execution context中执行,每个函数的调用有个专属的execution context。注意,用eval函数执行的代码有个独特的execution context.(原文中说eval函数没常被程序员应用,确实如果掌握闭包使用后,还是可以避免一些eval使用的)。在section 10.2 of ECMA 262 (3rd edition)中详细讲述的execution context.
当一个函数被调用,那相应的execution context被建立,如果另外的函数(或同一个函数递归调用),那新的execution context被建立,直到函数return(对于递归调用,execution context是独立的)。因此,javascript代码的执行会建立很多的execution contexts.
当一个函数的execution context被建立(javascript中有global和function两种,eval没讨论),按照顺序,有几个事情要发生。
(1)在一个函数的execution context中,一个"Activation"对象被建立(我在其它文章中叫调用对象)。the activation被另外规范解释。你可以把它当成一个对象,因为它有对象的属性,但它不是一般对象,它没有原型对象,并不能被javascript代码直接引用。
(2)建立一个arguments对象,它和数组类似,以整数为索引来访问值,表示函数的参数。它有length和callee属性。这个arguments对象被当成activation对象的属性。在函数内可以直接访问得到。
(3)下一步,execution context被分配一个 scope属性(scope chain后面讲到,我们可以把scope理解成对象的一个scope属性,值是scope chain)。一个scope由一列对象组成(或叫chain)。每个函数对象也有由chain组成的scope属性。函数的scope=Activation object+上级对象的scope的属性.(这里的scope可以理解成servlet中的chain,一系列请求组成的链。)
(4)Activation object的实例化。Activation object(调用对象)可以看作Variable(变量)。
function fun(a,b){};fun('p'); a和b会当成调用对象的属性,但函数调用是参数不够,b的值为undefined。如果函数内有inner function,当成属性赋值给调用对象。变量实例化最后把local variables(函数内部声名的变量) 当成调用对象的参数。调用对象的属性 包括函数的参数、内部变量。
(5)在函数内,local variables作为调用对象的属性出现,function (a){alert(s); var s='a';}调用时,s的值是unidefine,直到执行到赋值语句后才有值。
(6)arguments属性是以索引标识的参数,它和显示声明的参数是重复的,值也相同。如果local变量的签名和参数相同,那么它们三者一个变化,其它都会相应改变值。见下例
function a(p){alert( arguments [0]);alert(p);var p=1;alert(p);alert( arguments [0]);};a(0);
(7)最后,为this关键字设置值。可能 new Function()的有些疑问。关于this关键字,感觉自己还没有彻底理解。this关键字关联于执行时的作用域,而非定义时的作用域。(The this keyword is relative to the execution context, not the declaration context )
global execution context 的过程和上面有些不同,它没有arguments也不需要定义Activation object。global execution context也不需要scope chain,因为scope chain只有一个,就是global object.它的变量实例化过程和inner function其实都是根变量和函数,就是global对象的属性。global execution context用this应用global对象,在浏览器中为window.
2、Scope chains and [[scope]]
The scope chain of the execution context for a function call is constructed by adding the execution context's Activation/Variable object to the front of the scope chain held in the function object's [[scope]] property。我理解每个函数执行环境都有scope chain,子函数(inner function)的scope chain包括它的父函数的scope chain,如此递归对global对象。
在ECMAScript中,函数是个对象,它们可以用function声明,或function表达式声明,或Function构造函数初始化。
用Function构造的函数对象一直有个scope属性,指向的scope chain 仅包括 global 对象。
用function表达式定义的函数对象,这类函数对象的scope chain被分配到内部的scope 属性。
(1)简单的global函数,例如:-
function exampleFunction(formalParameter){
... // function body code
}
在global execution context的变量实例化阶段,the corresponding function object 被创建。global execution context有scope chain,只包含global object.因此,函数对象被分配一个指向global object的 scope属性( internal [[scope]] property)。
(2)A similar scope chain is assigned when a function expression is executed in the global context:-
var exampleFuncRef = function(){
... // function body code
}
这个例子scope chain情形与上类似。有个区别是函数对象在代码执行过程才创建。(见我以前文章)
(3)inner 函数的情形较为复杂,看下面代码:
function exampleOuterFunction(formalParameter){
function exampleInnerFuncitonDec(){
... // inner function body
}
... // the rest of the outer function body.
}
exampleOuterFunction( 5 );
outer函数在global execution context变量实例化阶段被创建,因此它的scope chain只包括global object.
当global代码执行到调用exampleOuterFunction时,一个新的execution context被创建,(Activation)调用对象也被创建。这个新的execution context的scope chain由两部分组成,新的调用对象在顶层,outer函数scope chain(只包括global object)在后。新的execution context的变量实例化阶段(outer 函数体内)导致inner函数对象被创建,这个inner函数对象的[[scope]] property 被指向上述的哪个scope chain,也就是调用对象和global object.注意inner function也有调用对象。
引用了 http://wj.cnblogs.com/archive/2006/04/22/381851.html 回复内的代码
<SCRIPT LANGUAGE="JavaScript">
<!--
//global 代码实例化阶段,它知道global object.
function createAClosure()
{
//当调用时,调用对象创建,execution context的scope chain 包括调用对象和global
//object.
var local = 0;
return function(){return ++local;}; //这个inner function 的scope //chain持有
//createAClosure的调用对象,所以也持有local的值
}
var c1 = createAClosure(); //调用对象和global object
var c2 = createAClosure(); //另外一个调用对象和global object
document.write(c1() + "<br/>");
document.write(c1() + "<br/>");
document.write(c1() + "<br/>");
document.write(c2() + "<br/>");
document.write(c2() + "<br/>");
//-->
</SCRIPT>
以上所有过程自动进行,代码不需要任何设置(造成很多人不知道闭包原因)。
scope chain 简单看来可以按照下面的代码来描述:
函数体外Execution context 的scope chain 只有 global.
function fun(){
函数体内Execution context 的scope chain fun的调用对象+global
function innerfun(){
inner函数体内Execution context 的scope chain innerfun的调用对象 + fun的调用对象 + global
}
}
但是ECMAScript提供的with表达式会修改scope chain.with表达式,我是能不用就不用了,<javascript权威指南>中也说with会造成性能的集聚下降。原文贴在下面。有时间再仔细研究。
The with statement evaluates an expression and if that expression is an object it is added to the scope chain of the current execution context (in front of the Activation/Variable object). The with statement then executes another statement (that may itself be a block statement) and then restores the execution context's scope chain to what it was before.
A function declaration could not be affected by a with statement as they result in the creation of function objects during variable instantiation, but a function expression can be evaluated inside a with statement:-
/* create a global variable - y - that refers to an object:- */
var y = {x:5}; // object literal with an - x - property
function exampleFuncWith(){
var z;
/* Add the object referred to by the global variable - y - to the
front of he scope chain:-
*/
with(y){
/* evaluate a function expression to create a function object
and assign a reference to that function object to the local
variable - z - :-
*/
z = function(){
... // inner function expression body;
}
}
...
}
/* execute the - exampleFuncWith - function:- */
exampleFuncWith();
When the exampleFuncWith function is called the resulting execution context has a scope chain consisting of its Activation object followed by the global object. The execution of the with statement adds the object referred to by the global variable y to the front of that scope chain during the evaluation of the function expression. The function object created by the evaluation of the function expression is assigned a [[scope]] property that corresponds with the scope of the execution context in which it is created. A scope chain consisting of object y followed by the Activation object from the execution context of the outer function call, followed by the global object.
When the block statement associated with the with statement terminates the scope of the execution context is restored (the y object is removed), but the function object has been created at that point and its [[scope]] property assigned a reference to a scope chain with the y object at its head.
3、Identifier Resolution
关于这部分我决定不按照原文直译。Identifier Resolution是一个过程,而不是具体的概念,我举个例子可能就明白了。
<SCRIPT LANGUAGE="JavaScript">
<!--
var s_global='global';//scope chain {global} 中
var s_outer='global';//scope chain {global} 中
var s_inner='global';//scope chain {global} 中
function outerfun(){//scope chain {global} 中
var s_outer='outer';//scope chain {outerfun调用对象,global}
pf('outer代码开始');
pf(s_global);//global
pf(s_outer);//outerfun调用对象
pf(s_inner);//global
function innerfun(){////scope chain {outerfun调用对象,global}
var s_inner='inner';//scope chain {innerfun调用对象,outerfun调用对象,global}
pf('inner代码开始');
pf(s_global);//global
pf(s_outer);//outerfun调用对象
pf(s_inner);//innerfun调用对象
}
return innerfun;
}
function pf(msg){document.writeln('</br>'+msg);};
pf('global代码开始');
pf(s_global);//global
pf(s_outer);//global
pf(s_inner);//global
var a=outerfun();
a();
pf('第二个函数开始------------------------');
var b=outerfun();
b();
//-->
</SCRIPT>
其实Identifier Resolution就是属性查找的过程。 先从scope chain 的第一个对象开始找,如果找不到再从scope chain的第二个对象找, global对象始终是scope chain 的最后一个对象,如果global object中也找不到属性,那为undefined.
有两个注意点:
如果可能,这个查找过程会对对象的prototype(原型对象)查找。先找实例属性,再找原型属性。见我的其它文章。
在函数内,这个函数的调用对象包括的参数,local变量,inner函数等。
如果有对javascript语言感兴趣的,欢迎交流批评。
http://www.blogjava.net/zkjbeyond/category/10156.html
参考:
《javascript权威指南》
http://jibbering.com/faq/faq_notes/closures.html
闭包(续)
书接上回,继续闭包。Closures
1、自动的垃圾回收
ECMAScript有自动的垃圾回收机制。与java类似。但是规范也没有对该机制详细定义,而是让浏览器等规范实现厂家来实现,各种浏览器实现不一样,垃圾回收的算法也不同。好象ie的实现会出现内存溢出问题。对我我们来说注意到这点就够了,后面会提到如何避免ie的这个bug.
关于上篇提到的execution context,调用对象,参数,scope chain 等等都需要内存,垃圾回收机制会在适当时候释放内存。
2、闭包如何形成
通俗的说,当一个(outer)函数的返回类型是(inner)函数类型时,这个被返回的inner函数斥又outer函数的scope chain,这时候闭包形成。 如下例:
function exampleClosureForm(arg1, arg2){
var localVar = 8;
function exampleReturned(innerArg){
return ((arg1 + arg2)/(innerArg + localVar));
}
/* return a reference to the inner function defined as -
exampleReturned -:-
*/
return exampleReturned;
}
var globalVar = exampleClosureForm(2, 4);
现在exampleClosureForm(2, 4)返回的inner函数不能被垃圾回收,因为它被变量globalVar持有,并可执行globalVar(n)。
但是内部的原理没有表面上那么简单。现在globalVar是个函数对象,它的[[scope]] property 指向一个scope chain,而这个scope chain 包括 exampleClosureForm函数的调用对象+global对象。所以垃圾回收不能回收这部分内存。
一个闭包形成了。inner函数对象有自己的变量,也可以访问exampleClosureForm函数调用过程中的参数,local变量等。
在上面的例子中,globalVar(n)执行时,在通过调用对象可以访问到exampleClosureForm(2, 4)执行过程中的参数,local变量等。arg1 = 2,arg2 = 4 ,localVar=8,这些属性都通过调用对象"ActOuter1"可以得到。
如果增加以下代码,又返回另外一个inner 函数。
var secondGlobalVar = exampleClosureForm(12, 3);
exampleClosureForm(12, 3)会引起新的调用对象创建,我们定义为ActOuter2。这个过程中,arg1 = 12,arg2 = 3 ,localVar=8。第二个闭包形成了.
下面考虑返回的inner函数执行过程。如globalVar(2)。新的execution context、调用对象(ActInner)被创建。现在的scope chain是 ActInner1->ActOuter1->global object. 函数返回是 ((2 + 4)/(2 + 8)).
如果是secondGlobalVar(5)被执行情况是什么呢?现在的scope chain是ActInner2-> ActOuter2-> global object.函数返回是 ((12 + 3)/(5 + 8)).
通过比较,这两个inner函数互不干扰的执行。如果嵌套更多的函数的话,与上面所诉类似。明白的javascript的闭包,从这个方面可能就能体会到它比java等慢n个数量级的原因。
3、闭包能做什么(例子)
(1)
function callLater(paramA, paramB, paramC){
return (function(){
paramA[paramB] = paramC;
});
}
var functRef = callLater(elStyle, "display", "none");
hideMenu=setTimeout(functRef, 500);
想象我们做颜色渐变,或者动画的时候吧。上面提供的函数多幽雅。
(2)
function associateObjWithEvent(obj, methodName){
return (function(e){
e = e||window.event;
return obj[methodName](e, this);
});
}
function DhtmlObject(elementId){
var el = getElementWithId(elementId);
if(el){
el.onclick = associateObjWithEvent(this, "doOnClick");
el.onmouseover = associateObjWithEvent(this, "doMouseOver");
el.onmouseout = associateObjWithEvent(this, "doMouseOut");
}
}
DhtmlObject.prototype.doOnClick = function(event, element){
... // doOnClick method body.
}
DhtmlObject.prototype.doMouseOver = function(event, element){
... // doMouseOver method body.
}
DhtmlObject.prototype.doMouseOut = function(event, element){
... // doMouseOut method body.
}
......
又一种注册事件的方法。我觉得作者的这种实现可谓精妙。大大的开阔了我的思路。我们可以为我们的UI事件绑定到对象上,可以很好的重用代码。另外比起prototype.js的时间注册来说简单点。
(3)
var getImgInPositionedDivHtml = (function(){
var buffAr = [
'<div id="',
'', //index 1, DIV ID attribute
'" style="position:absolute;top:',
'', //index 3, DIV top position
'px;left:',
'', //index 5, DIV left position
'px;width:',
'', //index 7, DIV width
'px;height:',
'', //index 9, DIV height
'px;overflow:hidden;/"><img src=/"',
'', //index 11, IMG URL
'/" width=/"',
'', //index 13, IMG width
'/" height=/"',
'', //index 15, IMG height
'/" alt=/"',
'', //index 17, IMG alt text
'/"><//div>'
];
return (function(url, id, width, height, top, left, altText){
buffAr[1] = id;
buffAr[3] = top;
buffAr[5] = left;
buffAr[13] = (buffAr[7] = width);
buffAr[15] = (buffAr[9] = height);
buffAr[11] = url;
buffAr[17] = altText;
return buffAr.join('');
}); //:End of inner function expression.
})();
这种匿名函数的调用在dojo中见过,现在再看,感觉不一样。
以上是原作者的例子,我抄过来的。下次我准备深入研究一下闭包能给我们开发js类库提供什么更好的思路。感觉现在很多人对闭包了解不多,经过这段时间的思考,利用javascript中的闭包,代码偶合性会更低。
关于Javascript的内存泄漏问题的整理稿
常规循环引用内存泄漏和Closure内存泄漏
要了解javascript的内存泄漏问题,首先要了解的就是javascript的GC原理。
我记得原来在犀牛书《JavaScript: The Definitive Guide》中看到过,IE使用的GC算法是计数器,因此只碰到循环 引用就会造成memory leakage。后来一直觉得和观察到的现象很不一致,直到看到Eric的文章,才明白犀牛书的说法没有说得很明确,估计该书成文后IE升级过算法吧。在IE 6中,对于javascript object内部,jscript使用的是mark-and-sweep算法,而对于javascript object与外部object(包括native object和vbscript object等等)的引用时,IE 6使用的才是计数器的算法。
Eric Lippert在http://blogs.msdn.com/ericlippert/archive/2003/09/17/53038.aspx一文中提到IE 6中JScript的GC算法使用的是nongeneration mark-and-sweep。对于javascript对算法的实现缺陷,文章如是说:
"The benefits of this approach are numerous, but the principle benefit is that circular references are not leaked unless the circular reference involves an object not owned by JScript. "
也就是说,IE 6对于纯粹的Script Objects间的Circular References是可以正确处理的,可惜它处理不了的是JScript与Native Object(例如Dom、ActiveX Object)之间的Circular References。
所以,当我们出现Native对象(例如Dom、ActiveX Object)与Javascript对象间的循环引用时,内存泄露的问题就出现了。当然,这个bug在IE 7中已经被修复了[http://www.quirksmode.org/blog/archives/2006/04/ie_7_and_javasc.html]。
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp 中有个示意图和简单的例子体现了这个问题:
< head >
< script language = " JScript " >
var myGlobalObject;
function SetupLeak() // 产生循环引用,因此会造成内存泄露
{
// First set up the script scope to element reference
myGlobalObject =
document.getElementById( " LeakedDiv " );
// Next set up the element to script scope reference
document.getElementById( " LeakedDiv " ).expandoProperty =
myGlobalObject;
}
function BreakLeak() // 解开循环引用,解决内存泄露问题
{
document.getElementById( " LeakedDiv " ).expandoProperty =
null ;
}
</ script >
</ head >
< body onload = " SetupLeak() " onunload = " BreakLeak() " >
< div id = " LeakedDiv " ></ div >
</ body >
</ html > 上面这个例子,看似很简单就能够解决内存泄露的问题。可惜的是,当我们的代码中的结构复杂了以后,造成循环引用的原因开始变得多样,我们就没法那么容易观察到了,这时候,我们必须对代码进行仔细的检查。
尤其是当碰到Closure,当我们往Native对象(例如Dom对象、ActiveX Object)上绑定事件响应代码时,一个不小心,我们就会制造出Closure Memory Leak。其关键原因,其实和前者是一样的,也是一个跨javascript object和native object的循环引用。只是代码更为隐蔽,这个隐蔽性,是由于javascript的语言特性造成的。但在使用类似内嵌函数的时候,内嵌的函数有拥有一个reference指向外部函数的scope,包括外部函数的参数,因此也就很容易造成一个很隐蔽的循环引用,例如:
DOM_Node.onevent ->function_object.[ [ scope ] ] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。
[http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp]有个例子极深刻地显示了该隐蔽性:
< html >
< head >
< script language = " JScript " >
function AttachEvents(element)
{
// This structure causes element to ref ClickEventHandler //element有个引用指向函数ClickEventHandler()
element.attachEvent( " onclick " , ClickEventHandler);
function ClickEventHandler()
{
// This closure refs element //该函数有个引用指向AttachEvents(element)调用Scope,也就是执行了参数element。
}
}
function SetupLeak()
{
// The leak happens all at once
AttachEvents(document.getElementById( " LeakedDiv " ));
}
</ script >
</ head >
< body onload = " SetupLeak() " onunload = " BreakLeak() " >
< div id = " LeakedDiv " ></ div >
</ body >
</ html >
还有这个例子在IE 6中同样原因会引起泄露
function leakmaybe() {
var elm = document.createElement( " DIV " );
elm.onclick = function () {
return 2 + 2 ;
}
}
for ( var i = 0 ; i 10000 ; i ++ ) {
leakmaybe();
}
btw:
关于Closure的知识,大家可以看看http://jibbering.com/faq/faq_notes/closures.html这篇文章,习惯中文也可以看看zkjbeyond的blog,他对Closure这篇文章进行了简要的翻译:http://www.blogjava.net/zkjbeyond/archive/2006/05/19/47025.html。之所以会有这一系列的问题,关键就在于javascript是种函数式脚本解析语言,因此javascript中“函数中的变量的作用域是定义作用域,而不是动态作用域”,这点在犀牛书《JavaScript: The Definitive Guide》中的“Funtion”一章中有所讨论。
http://support.microsoft.com/default.aspx?scid=KB;EN-US;830555中也对这个问题举了很详细的例子。
一些 简单的解决方案
目前大多数ajax前端的javascript framework都利用对事件的管理,解决了该问题。
如果你需要自己解决这个问题,可以参考以下的一些方法:
-
http://youngpup.net/2005/0221010713 中提到:可以利用递归Dom树,解除event绑定,从而解除循环引用:
if (window.attachEvent) {
var clearElementProps = [
'data',
'onmouseover',
'onmouseout',
'onmousedown',
'onmouseup',
'ondblclick',
'onclick',
'onselectstart',
'oncontextmenu'
];
window.attachEvent("onunload", function() {
var el;
for(var d = document.all.length;d--;){
el = document.all[d];
for(var c = clearElementProps.length;c--;){
el[clearElementProps[c]] = null;
}
}
});
}
-
而http://novemberborn.net/javascript/event-cache一文中则通过增加EventCache,从而给出一个相对结构化的解决方案
/* EventCache Version 1.0
Copyright 2005 Mark Wubben
Provides a way for automagically removing events from nodes and thus preventing memory leakage.
See <http://novemberborn.net/javascript/event-cache> for more information.
This software is licensed under the CC-GNU LGPL <http://creativecommons.org/licenses/LGPL/2.1/>
*/
/* Implement array.push for browsers which don't support it natively.
Please remove this if it's already in other code */
if (Array.prototype.push == null ){
Array.prototype.push = function (){
for ( var i = 0 ; i < arguments.length; i ++ ){
this [ this .length] = arguments[i];
};
return this .length;
};
};
/* Event Cache uses an anonymous function to create a hidden scope chain.
This is to prevent scoping issues. */
var EventCache = function (){
var listEvents = [];
return {
listEvents : listEvents,
add : function (node, sEventName, fHandler, bCapture){
listEvents.push(arguments);
},
flush : function (){
var i, item;
for (i = listEvents.length - 1 ; i >= 0 ; i = i - 1 ){
item = listEvents[i];
if (item[ 0 ].removeEventListener){
item[ 0 ].removeEventListener(item[ 1 ], item[ 2 ], item[ 3 ]);
};
/* From this point on we need the event names to be prefixed with 'on" */
if (item[ 1 ].substring( 0 , 2 ) != " on " ){
item[ 1 ] = " on " + item[ 1 ];
};
if (item[ 0 ].detachEvent){
item[ 0 ].detachEvent(item[ 1 ], item[ 2 ]);
};
item[ 0 ][item[ 1 ]] = null ;
};
}
};
}();
-
使用方法也很简单:
<script type="text/javascript">
function addEvent(oEventTarget, sEventType, fDest){
if(oEventTarget.attachEvent){
oEventTarget.attachEvent("on" + sEventType, fDest);
} elseif(oEventTarget.addEventListener){
oEventTarget.addEventListener(sEventType, fDest, true);
} elseif(typeof oEventTarget[sEventType] == "function"){
var fOld = oEventTarget[sEventType];
oEventTarget[sEventType] = function(e){ fOld(e); fDest(e); };
} else {
oEventTarget[sEventType] = fDest;
};
/* Implementing EventCache for all event systems */
EventCache.add(oEventTarget, sEventType, fDest, true);
};
function createLeak(){
var body = document.body;
function someHandler(){return body;
};
addEvent(body, "click", someHandler);
};
window.onload = function(){
var i = 500;
while(i > 0){
createLeak();
i = i - 1;
}
};
window.onunload = EventCache.flush;
</script>
-
http://talideon.com/weblog/2005/03/js-memory-leaks.cfm 一文中的方法类似:
/*
* EventManager.js
* by Keith Gaughan
*
* This allows event handlers to be registered unobtrusively, and cleans
* them up on unload to prevent memory leaks.
*
* Copyright (c) Keith Gaughan, 2005.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* (CPL) which accompanies this distribution, and is available at
* http://www.opensource.org/licenses/cpl.php
*
* This software is covered by a modified version of the Common Public License
* (CPL), where Keith Gaughan is the Agreement Steward, and the licensing
* agreement is covered by the laws of the Republic of Ireland.
*/
// For implementations that don't include the push() methods for arrays.
if ( ! Array.prototype.push) {
Array.prototype.push = function (elem) {
this [ this .length] = elem;
}
}
var EventManager = {
_registry: null ,
Initialise: function () {
if ( this ._registry == null ) {
this ._registry = [];
// Register the cleanup handler on page unload.
EventManager.Add(window, " unload " , this .CleanUp);
}
},
/* *
* Registers an event and handler with the manager.
*
* @param obj Object handler will be attached to.
* @param type Name of event handler responds to.
* @param fn Handler function.
* @param useCapture Use event capture. False by default.
* If you don't understand this, ignore it.
*
* @return True if handler registered, else false.
*/
Add: function (obj, type, fn, useCapture) {
this .Initialise();
// If a string was passed in, it's an id.
if ( typeof obj == " string " ) {
obj = document.getElementById(obj);
}
if (obj == null || fn == null ) {
return false ;
}
// Mozilla/W3C listeners?
if (obj.addEventListener) {
obj.addEventListener(type, fn, useCapture);
this ._registry.push({obj: obj, type: type, fn: fn, useCapture: useCapture});
return true ;
}
// IE-style listeners?
if (obj.attachEvent && obj.attachEvent( " on " + type, fn)) {
this ._registry.push({obj: obj, type: type, fn: fn, useCapture: false });
return true ;
}
return false ;
},
/* *
* Cleans up all the registered event handlers.
*/
CleanUp: function () {
for ( var i = 0 ; i < EventManager._registry.length; i ++ ) {
with (EventManager._registry[i]) {
// Mozilla/W3C listeners?
if (obj.removeEventListener) {
obj.removeEventListener(type, fn, useCapture);
}
// IE-style listeners?
else if (obj.detachEvent) {
obj.detachEvent( " on " + type, fn);
}
}
}
// Kill off the registry itself to get rid of the last remaining
// references.
EventManager._registry = null ;
}
};使用起来也很简单
<html><head>
<script type=text/javascript src=EventManager.js></script>
<script type=text/javascript>
function onLoad() {
EventManager.Add(document.getElementById(testCase),click,hit );
returntrue;
}
function hit(evt) {
alert(click);
}
</script>
</head>
<body onload='javascript: onLoad();'>
<div id='testCase' style='width:100%; height: 100%; background-color: yellow;'>
<h1>Click me!</h1>
</div>
</body>
</html>
- google map api同样提供了一个类似的函数用在页面的unload事件中,解决Closure带来的内存泄露问题。
-
当然,如果你不嫌麻烦,你也可以为每个和native object有关的就阿vascript object编写一个destoryMemory函数,用来手动调用,从而手动解除Dom对象的事件绑定。
-
还有一种就是不要那么OO,抛弃Dom的一些特性,用innerHTML代替appendChild,避开循环引用。详细见http://birdshome.cnblogs.com/archive/2005/02/16/104967.html中的讨论贴。
Cross-Page Leaks
Cross-Page Leaks和下一节提到的Pseudo-Leaks在我看来,就是IE的bug, 虽然MS死皮赖脸不承认:)
大家可以看看这段例子代码:
< html >
< head >
< script language = " JScript " >
function LeakMemory() // 这个函数会引发Cross-Page Leaks
{
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() // 而这个函数不会引发Cross-Page Leaks
{
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 onclick = " LeakMemory() " > Memory Leaking Insert </ button >
< button onclick = " CleanMemory() " > Clean Insert </ button >
< div id = " hostElement " ></ div >
</ body >
</ html >
LeakMemory和CleanMemory这两段函数的唯一区别就在于他们的代码的循序,从代码上看,两段代码的逻辑都没有错。
但LeakMemory却会造成泄露。原因是LeakMemory()会先建立起parentDiv和childDiv之间的连接,这时候,为了让 childDiv能够获知parentDiv的信息,因此IE需要先建立一个临时的scope对象。而后parentDiv建立了和 hostElement对象的联系,parentDiv和childDiv直接使用页面document的scope。可惜的是,IE不会释放刚才那个临时的scope对象的内存空间,直到我们跳转页面,这块空间才能被释放。而CleanMemory函数不同,他先把parentDiv和 hostElement建立联系,而后再把childDiv和parentDiv建立联系,这个过程不需要单独建立临时的scope,只要直接使用页面 document的scope就可以了, 所以也就不会造成内存泄露了
详细原因,大家可以看看http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp这篇文章。
btw:
IE 6中垃圾回收算法,就是从那些直接"in scope"的对象开始进行mark清除的:
Every variable which is "in scope" is called a "scavenger". A scavenger may refer to a number, an object, a string, whatever. We maintain a list of scavengers – variables are moved on to the scav list when they come into scope and off the scav list when they go out of scope.
Pseudo-Leaks
这个被称为“秀逗泄露”真是恰当啊:)
看看这个例子:
< 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 onclick = " LeakMemory() " > Memory Leaking Insert </ button >
< script id = " hostElement " > function foo() { } </ script >
</ body >
</ html >
MS是这么解释的,这不是内存泄漏。如果您创建了许多无法获得也无法释放的对象,那才是内存泄漏。在这里,您将创建许多元素,Internet Explorer 需要保存它们以正确呈现页面。Internet Explorer 并不知道您以后不会运行操纵您刚刚创建的所有这些对象的脚本。当页面消失时(当您浏览完,离开浏览器时)会释放内存。它不会泄漏。当销毁页面时,会中断循环引用。
唉~~~
详细原因,大家可以看看http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp这篇文章。
其它一些琐碎的注意点
变量定义一定要用var,否则隐式声明出来的变量都是全局变量,不是局部变量;
全局变量没用时记得要置null;
注意正确使用delete,删除没用的一些函数属性;
注意正确使用try...cache,确保去处无效引用的代码能被正确执行;
open出来的窗口即使close了,它的window对象还是存在的,要记得删除引用;
frame和iframe的情况和窗口的情况类似。
参考资料
http://jibbering.com/faq/faq_notes/closures.html
http://javascript.weblogsinc.com/2005/03/07/javascript-memory-leaks/
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp
http://72.14.203.104/search?q=cache:V9Bt4_HBzQ8J:jgwebber.blogspot.com/2005/01/dhtml-leaks-like-sieve.html+DHTML+Leaks+Like+a+Sieve+&hl=zh-CN&ct=clnk&cd=9 (这是DHTML Leaks Like a Sieve)一文在google上的cache,原文已经连不上了)
http://spaces.msn.com/siteexperts/Blog/cns!1pNcL8JwTfkkjv4gg6LkVCpw!338.entry
http://support.microsoft.com/default.aspx?scid=KB;EN-US;830555
http://www.ajaxtopics.com/leakpatterns.html
http://blogs.msdn.com/ericlippert/archive/2003/09/17/53028.aspx
http://www.quirksmode.org/blog/archives/2005/02/javascript_memo.html
http://youngpup.net/2005/0221010713
http://blogs.msdn.com/ericlippert/archive/2003/09/17/53038.aspx =
http://support.microsoft.com/kb/266071/EN-US ==>IE 5.0至5.5一些版本中的GC bug
http://www.quirksmode.org/blog/archives/2006/04/ie_7_and_javasc.html ==>ie 7的改进
http://erik.eae.net/archives/2006/04/26/23.23.02/ ==>ie 7的改进
http://www.feedbackarchive.com/spamvampire/today.html ==> Try this script for memory leaks - it leaked 50 megabytes in 15 minutes with firefox on linux:
http://birdshome.cnblogs.com/archive/2005/02/15/104599.html
http://www.quirksmode.org/dom/innerhtml.html
http://www.crockford.com/javascript/memory/leak.html
《JavaScript: The Definitive Guide》4th Edition
http://outofhanwell.com/ieleak/index.php?title=Main_Page
不过这里的“内存溢出”似乎应该是“内存泄漏”。# re: 关于Javascript的内存泄漏问题的整理稿 @THEMAX
SetupLeak() 函数中
myGlobalObject = document.getElementById( " LeakedDiv " );
则 myGlobalObject有了一个指向DOM对象 " LeakedDiv " 的引用,
而后
document.getElementById( " LeakedDiv " ).expandoProperty = myGlobalObject;
使得DOM对象 " LeakedDiv " 有一个属性expandoProperty引用指向myGlobalObject;
这样myGlobalObject和DOM对象 " LeakedDiv "分别有引用指向对方,所以就循环引用了。
函数BreakLeak() 则清除了DOM对象" LeakedDiv " 向myGlobalObject的引用,
解决循环引用问题。
============================
如果你问的是第二段代码,那么就比较复杂了。
这涉及到Closure的问题,你可以看看文章中给出的其他链接文章,例如zkjbeyond对Closure一文的翻译。
大致的原因是因为ClickEventHandler()是AttachEvents()的一个内嵌函数,
AttachEvents()函数执行后,把ClickEventHandler()函数绑定在element对象的"onclick"事件上。这样,element对象就有个间接引用指向ClickEventHandler()函数的一个实例。
又正是因为ClickEventHandler()是一个内嵌函数,因此会为这个实例构造一个scope,这个scope中包括了父函数,也就是AttachEvents()函数中的局部变量和参数,而这儿的参数刚好就是element,所以ClickEventHandler()函数的实例同时又间接引用了element。
说得有些绕.......呵呵
# re: 关于Javascript的内存溢出问题的整理稿