写在前面的话:
试图翻译自 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(嵌套函数):函数可以定义在另外一个函数里面(关于嵌套函数可以看看
我估计以下代码就是一个闭包。
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 回复内的代码
以上所有过程自动进行,代码不需要任何设置(造成很多人不知道闭包原因)。
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表达式,我是能不用就不用了,
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是一个过程,而不是具体的概念,我举个例子可能就明白了。
其实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
不过这里的“内存溢出”似乎应该是“内存泄漏”。
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。
说得有些绕.......呵呵