首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 网站开发 > Web前端 >

原形、作用域、闭包的完整解释(二)

2012-10-27 
原型、作用域、闭包的完整解释(二)??? 到目前为止,这些都是自动完成的,并且由构建和执行源码(的机制)控制。执

原型、作用域、闭包的完整解释(二)

??? 到目前为止,这些都是自动完成的,并且由构建和执行源码(的机制)控制。执行环境中的作用域链定义了所创建的函数对象的[[scope]]属性,同时函数对象的[[scope]]属性为它们的执行环境定义了作用域(和对应的活动对象一起)。但是ECMAScript提供了with语句用来作为修改作用域链的手段。
??? with语句对一个表达式求值,如果这个表达式是一个对象,它将会被添加到当前执行环境的作用域链中(在活动对象/可变对象前面)。with语句然后执行其它语句(这本身可能是一个语句块)然后恢复执行环境的作用域链为之前的值。
??? 一个函数声明不会被with语句影响,因为它们会在变量初始化的时候导致创建函数对象,但是一个函数表达式可以在一个with段中被执行:
/* 创建一个全局变量y,引用一个对象*/
var y = {x:5}; //对象字面值,有一个x属性
function exampleFuncWith(){
??? var z;
/*
??? ? 将变量y引用的对象添加到作用域链的最前端
??? */
??? with(y){
??????? /*
??? ??? ??? 对函数表达式求值
??? ??? ??? 创建一个函数对象,将这个函数对象的引用赋值给局部变量z
??????? */
??????? z = function(){
??????????? ... // inner function expression body;
??????? }
??? }
??? ...
}

/* 执行exampleFuncWith 函数*/
exampleFuncWith();
??? 当exampleFuncWith函数被调用时,结果执行环境拥有一个作用域链,由它的活动对象加上全局对象组成。with语句执行,当函数表达式被求值时,全局变量y引用的对象被添加到这个作用域链前面。由执行函数表达式创建的函数对象被分配了一个[[scope]]属性,它对应于被创建时执行环境的作用域。这个作用域链由对象y加上调用外部函数时执行环境中的活动对象,再加上全局对象组成。
??? 当with语句中的代码块结束时,执行环境的作用域被恢复(y对象被删除),但是函数对象已经在那一点被创建,并且它的[[scope]]属性被赋值成了一个作用域链的引用,它以y对象开头。
标识符解析
??? 标识符根据作用域链解析。ECMA 262把this当作了一个关键字而不是标识符,这是不无道理的,因为它总是根据它被使用时执行环境中的this值解析,而没有引用作用域链。
??? 标识符解析从作用域链的第一个对象开始。它被检查,用来看看是否有与属性名称对应标识符。因为作用域链是一个对象链,这个检查包含该对象的原型链(如果有)。如果在作用域链中的第一个对象上没有找到对应的值,查找过程在下一个对象上进行。如此等等,直到作用域链中(或者其原型)的一个对象拥有一个属性的名字与标识符对应,或者作用域链耗尽为止。
??? 在标识符上进行的操作与以上描述的访问对象原型的方式一样。在作用域链中确定的拥有对应属性的对象,会替代属性访问器中的对象,并且使用标识符作为这个对象的属性名。全局对象总是位于作用域链的最后。
??? 因为绑定到调用函数时的执行环境将会拥有活动/可变对象位于链的顶部,函数体中使用的标识符将会首先做有效性检测,看看它们是否与形参、内部函数声明的名称或者局部变量相一致。这些将会被解析成活动对象/可变对象的命名属性。
闭包
自动垃圾收集
??? ECMAScript使用自动垃圾收集。规范没有规定细节,把它留给具体实现去考虑,一些已知的实现给它们的垃圾收集操作一个很低的优先级。但是总的思路是,如果一个对象变成孤立的(有没留下到它的引用供执行的代码访问),它就变成了一个需要进行垃圾收集的变量,在未来的某个时间点它会被销毁,并且它所消耗的所有资源都会被释放并归还给系统,以便重复利用。
??? 这通常是退出执行环境时反生的情况。作用域链、活动/可变对象以及所有在执行环境中创建的对象,包括函数对象,将不再可以访问并且将会变成可被垃圾收集的变量。
构造闭包
??? 通过返回一个函数对象,它在一次函数调用的执行环境中被创建,从这次函数调用中,把这个内部函数的引用赋值给另一个对象的属性,或者直接把这个函数对象的引用分配给,例如一个全局变量、一个全局可见对象的属性,或者赋给在调用外部函数时作为参数传递的对象引用。例如:
function exampleClosureForm(arg1, arg2){
??? var localVar = 8;
??? function exampleReturned(innerArg){
??????? return ((arg1 + arg2)/(innerArg + localVar));
??? }
/*
??? ??? 返回一个定义为exampleReturned的内部函数引用
??? */
??? return exampleReturned;
}
var globalVar = exampleClosureForm(2, 4);
??? 现在,调用exampleClosureForm函数时执行环境中创建的函数对象不能被垃圾收集,因为它被全局变量所引用并且仍然可以访问,它甚至可以使用globalVar(n)这种方式来执行。
??? 因为函数对象现在被globalVar引用,事情变得有一点小复杂,这个函数创建了一个[[scope]]属性,引用了一个属于执行环境的作用域链,包含了活动/可变对象(还有全局对象),这个函数是在执行环境中被创建的。现在,活动/可变对象也不能被垃圾收集,因为对象被globalVar引用,函数执行时将会需要从它的[[scope]]属性中把整个作用域链添加到调用它时所创建执行环境的作用域中去。
??? 一个闭包形成了。内部函数对象拥有任意变量和活动/可变对象,这些对象由环境绑定到函数的作用域链上。
??? 活动/可变对象现在被限制,并被作用域链引用,作用域链被赋值给函数对象内部的[[scope]]属性,函数对象被变量globalVar引用。活动/可变对象和它的状态及其属性一起被保存。调用内部函数时,执行环境中的作用域解析将会解析标识符,活动/可变对象相应的命名属性就是这个对象的属性。这些属性值仍然可以读写,即使创建它们的执行环境已经退出。
??? 在以上例子中,在外部函数返回之后(退出它的执行环境),活动/可变对象有一个表示形参值、内部函数定义和局部变量的状态。arg1属性值为2,arg2属性值为4,loclaVar值为8,exampleReturned属性是从外部函数返回的一个指向内部函数对象的引用(为了方便,在后面的讨论中我们将会把这个活动/可变对象称作”ActOuter1”)。
??? 如果exampleClousureForm函数再次被调用作:
??? var secondGlobalVar = exampleClosureForm(12, 3);
??? 一个新的执行环境将会被创建,还有一个新的活动对象。然后一个新的函数对象将会被返回,拥有它自己唯一的[[scope]]属性,引用一个作用域链,包含来自第二个执行环境的活动对象,arg1为12,arg2为3(为了方便,在后面的讨论中我们将会把这个活动/可变对象称作”ActOuter2)
??? 通过再次执行exampleClosureForm,形成了第二个不同的闭包。
??? 通过执行exampleClosureForm函数创建了两个函数对象,它们的引用被分别赋值给了全局变量globalVar和secondGlobalVar,函数对象返回表达式((arg1 + arg2)/(innerArg + localVar))。此表达式在四个标识符上进行了大量操作。这些标识符如何被解析对闭包的用法和价值非常重要。
??? 考虑使用globalVar(2)的形式执行globalVar所引用的函数对象。一个新的执行环境被创建,还有一个活动对象(我们将会称它做”ActInner1”),它将被添加到作用域链顶部,被所执行的函数对象中的[[scope]]属性引用。ActInner1被赋给了一个名为innerArg的属性,然后它的形参和参数值2被分配给它。新的执行环境的作用域链目前是:ActInner1->ActOuter1->global object。
??? 根据作用域链,标识符解析已经完成,所以为了返回表达式((arg1 + arg2)/(innerArg + localVar))的值,通过查找与标识符名称一致的属性,这些标识符的值将会被计算,在作用域链中的每个对象上依次进行。
??? 作用域链中的第一个对象就是ActInner1,它有一个名为innerArg的属性其,值为2。其它3个标识符都与ActOuter1的命名属性相对应;arg1为2,arg2为4,localVar为8。函数调用将会返回((2+4)/(2+8))。
??? 与被secondGlobalVar引用的其它相同函数对象相比较,secondGlobalVar(5)。把这个新执行环境中的活动对象叫做”ActInner2”,作用域链变成了:ActInner2->ActOuter2->global object。ActInner2返回innerArg为5,ActOuer2返回arg1,arg2,localVar分别12,3,8。最终返回的值为((12 + 3)/(5 + 8)).
??? 再次执行secondGlobalVar,一个新的活动对象出现在作用域链的头部,但是ActOuter2仍然是作用域链中的下一个对象,并且它的命名属性在解析标识符arg1,arg2和localVar时会被再次使用。
??? 这就是ECMAScript内部函数如何获取、保持、访问它被创建时的那个执行环境中的形参、声明的内部函数和变量的方式。并且就是闭包如何允许这些函数对象以何种形式保持到这些值的引用、读写它们,只要它继续存在。所创建内部函数执行环境中的活动/可变对象,存在于作用域链中,被函数对象的[[scope]]属性引用,直到所有对内部函数的引用都被释放并且变为可以进行垃圾收集为止(与它的作用域链中所有无效对象一起)。
??? 内部函数自己也可以有内部函数,从函数的执行环境中返回的内部函数形成的闭包自己又可以返回内部函数,并形成它自己的闭包。每层嵌套,执行环境都会获得额外的活动对象,此对象来自函数对象被创建的执行环境。ECMAScript规范要求作用域链是有限的,但是没有限定其长度。具体实现可以做一些实际的额外限制,但是具体的幅度还没有相关报告。迄今为止,内部函数的潜力已经超越了任何实现它们的人的期望。
我们可以用闭包做什么?
??? 奇怪的是,这个问题的答案是任何东西,一切。有人告诉我,闭包使得ECMAScript可以模拟任何东西,所以唯一的限制就是想象力和模拟实现的能力了。这有一点深奥,以一些更实际的东西开始可能更好。
例1:使用函数引setTimeout
??? 闭包的一个常见用途是在执行函数之前为函数提供参数。例如,把函数用作setTimeout函数的第一个参数在web浏览器环境很常用。
??? setTimout计划调用一个函数(或者一个javasctipt源码字符串,但是不在当前上下文中),作为其第一个参数提供,后面是一个以毫秒为单位的时间间隔(作为其第二个参数)。如果一段代码想使用setTimeout,它调用setTimeout函数并传递一个函数对象的引用作为第一个参数,和一个毫秒时间间隔作为第二个参数,但是函数对象的引用无法为这个计划执行的函数提供参数。
??? 但是,代码可以调用另一个函数,它返回一个内部函数的引用,把这个内部函数的引用传递给setTimeout函数。执行内部函数需要使用的参数在调用(外部)函数时传递,这个(外部)函数生成了内部函数。setTimeout执行这个内部函数而不传递参数,但是内部函数仍然可以访问调用外部函数时所提供的参数,外部函数返回了它(指内部函数):
function callLater(paramA, paramB, paramC){
/*
??? ??? 返回一个匿名内部函数,它是通过一个函数表达式创建的
??? */
??? return (function(){
??????? /*
??? ??? ??? 这个内部函数将会被setTimeout执行,并且当它被执行时
??? ??? ??? 它可以读取并操纵传递给外部函数的参数
??????? */
??????? paramA[paramB] = paramC;
??? });
}

...

/*
??? 调用这个函数,它将返回一个内部函数对象的引用,这个函数对象
??? 是在它的执行环境中被创建的 。传递参数传递给外部函数,这些参数
??? 是最终执行内部函数时需要的。
*/
var functRef = callLater(elStyle, "display", "none");



/*
??? 调用setTimeout函数,将赋值给functRef的内部函数引用作为第一个参数传递
*/
hideMenu=setTimeout(functRef, 500);
例2:将函数与对象实例方法关联
??? 有很多其它情况,当一个函数的引用被赋值,然后在未来某时被执行,为执行这个函数提供参数非常有用,并且在执行时提供不是非常合适,但是直到赋值时都无法确知。
??? 一个例子可能是,javascript对象被设计用来包装与特定DOM元素的交互。它有doOnClick,doMouseOver和doMouseOut几个方法,需要在DOM元素触发对应的事件时执行,但是可能有任意数量的javascript对象实例被创建并关联到不同DOM元素,并且单个对象实例并不知道它们将会如何受雇于实例化它们的代码。对象实例不知道如何全局地引用它们自己,因为它们不知道它们的实例会被赋值给哪个全局变量(如果存在)。
??? 所以问题是,执行一个事件处理函数,它关联了一个特定的javascript对象,并知道调用这个对象的哪个方法。
??? 以下例子使用了一个一般性的基于闭包的小函数,它把事件处理函数绑定到对象实例。安排事件处理程序调用对象实例的特定方法,传递事件对象和一个所关联元素的引用给对象的方法,并返回该方法的返回值。
/*
??? 一个通用函数,将一个对象关联到事件处理器。返回的
??? 内部函数将被用作事件处理器。对象实例以名为obj的参数传递
??? 对象上需要被调用方法的名字以名为methodName(字符串)的参数传递
*/
function associateObjWithEvent(obj, methodName){
/*
??? ??? 返回的内部函数将作为替代,扮演DOM元素事件处理器的角色
??? */
??? return (function(e){
??????? /*
??? ??? ??? 一般化的事件对象,在DOM标准的浏览器中,它将会被以名为e的
??? ??? ??? 参数传递,对于IE事件对象不会被作为参数传递给作为事件处理器
??? ??? ??? 的内部函数
??????? */
??????? e = e||window.event;//译者注:这是兼容IE事件对象的一个重要技巧
??????? /*
??? ??? ??? 事件处理器调用obj对象上的一个方法,其名称被字符串methodName持有
??? ??? ??? 传递一般化的事件以及元素的引用,事件处理器将会以this引用它们
??? ??? ??? (这能起作用是因为这个内部函数被作为这个元素的一个方法执行,因为它
??? ??? ??? 已经被作为一个事件处理函数赋值)
??????? */
??????? return obj[methodName](e, this);
??? });
}

/*
??? 这个构造函数创建对象,把它们自己关联到DOM元素,这些元素
??? 的ID被作为字符串传递给构造器。对象实例需要安排当相应元素触发onclick,??? onmouseover,onmouseout事件时,它们对象实例上的相应方法会被调用
*/
function DhtmlObject(elementId){
/*
??? ??? 一个函数被调用,用来获取DOM元素的引用(如果没有找到为null),
??? ??? 传递所需元素的ID作为参数。返回值将会被赋给局部变量el
??? */
??? var el = getElementWithId(elementId);
/*
??? ??? 因为if语句,el的值将会被隐含转换成布尔型,所以如果el引用一个对象
??? ??? 结果将会为true,如果el为null则结果为false。所以,接下来的代码块只有
??? ??? 在el变量引用一个DOM元素时才会被执行
??? */
??? if(el){
??????? /*
??? ??? ??? 为了给元素的事件处理器赋一个值,该对象调用associateObjWithEvent函??? ??? ??? ??? 数,指定自己(使用this关键字)作为需要在其中调用方法的对象,并且提供??? ??? ??? ??? 需要调用方法的名称。associateObjWithEvent函数将会返回一个内部函数引??? ??? ??? 用,它将会被赋值给DOM元素的事件处理器。这个内部函数将会调用??? ??? ??? ??? javascript对象上所需的方法响应事件:
??????? */
??????? 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.
}
??? 这样DhtmlObject的所有实例都可以把自己关联到自己感兴趣的DOM元素上,而不需要知道它们自己是如何被其它代码调用的任何细节,以及对全局命名空间的影响、与其它DhtmlObject实例冒冲突的风险。
例3:封装相关的功能
??? 闭包可以用来创建额外的作用域,可以用来组合互相关联和依赖的代码,通过这种方式最大限度地减少意外交互的风险。假设一个函数用来创建一个字符串并避免重复的连接操作(和创建许多中间字符串)期望是使用一个数组来顺序存储字符串片段,然后使用Array.prototype.join方法输出结果(使用一个空字符串作为它的参数)。数组将会用来作为一个输出缓冲区,但是在函数内部定义它将会导致每次执行函数时都会重复创建,如果数组中的唯一变量内容将会在每次调用函数时被重新分配,这可能是没有必要的。
??? 一种方法是把数组做成一个全局变量,这样它就可以被重复利用而无需重新创建。但是这样做的后果是,除了全局变量所引用的函数会使用这个数组之外,还存在第二个全局变量引用数组自身。其影响就是降低代码的受控程度,因为,如果它被用在其它地方,它的作者可能不记得既导入函数定义又导入数组定义。这也导致代码不能与其它代码方便地进行交互,因为除了保证函数名在全局命名空间中唯一之外,还必须保证它所依赖的数组在全局命名空间中名称唯一。
??? 闭包允许把这个缓冲区数组绑定(并整齐地包装)到依赖它的函数上,同时把缓冲区数组的属性名保持在全局命名空间之外,从而避免了命名冲突和意外交互的风险。
??? 这里的技巧是创建一个额外的执行环境,通过执行一个内联的函数表达式,让这个函数表达式返回一个内部函数,它将被外部代码使用。这个操作只执行一次,所以数组只会被创建一次,但是却可以被依赖它的函数重复使用。
??? 以下代码创建了一个会返回HTML字符串的函数,其中大部分是常量,但是这些常量字符序列需要插入调用函数时所提供的变量信息。
??? 从对一个函数表达式的内联执行,会返回一个内部函数的引用,并且赋值给一个全局变量,这样它就可以被当作全局函数调用。缓冲区数组定义为外部函数表达式的局部变量。它没有被暴露在全局命名空间中,并且没有必要被重复创建,无论使用它的函数何时被调用。
/*
??? 全局变量getImgInPositionedDivHtml被声明并赋值为内部函数表达式的引用,
??? 这个内部函数是对外部函数表达式的一次调用产生的。
??? 这个内部函数返回一个HTML字符串,描述了一个绝对定位的DIV,包装在IMG
??? 元素的周围,使得所有变量的属性值都被以调用函数时的参数提供:
*/
var getImgInPositionedDivHtml = (function(){
/*
??? ??? buffAr数组被赋值给外部函数表达式的一个局部变量。
??? ??? 它仅仅被创建一次,并且这个数组实例对内部函数有效,
??? ??? 所以每次执行这个内部函数时它都有效。
??? ??? 空字符串当前被用来作占位符,它们将会被内部函数插入到数组中
??? ??? (译者注:指它们将会被实际值替换):
??? */
??? 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>'
??? ];
/*
??? ??? 返回内部函数的引用,它是一个函数表达式执行的结果。
??? ??? 每次调用getImgInPositionedDivHtml( ... )时,这个内部函数
??? ??? 对象将会被调用:
??? */
??? 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('');
??? }); //内部函数表达式结束
})();
/*外部函数表达式的内联调用*/
??? 如果一个函数依赖一个(或者多个)其它函数,并且这些其它的函数不想直接受雇于任何其它代码,那么可以使用同样的技术把这些函数组织到需要被公开暴露的函数中。把一个复杂的多个函数处理的过程制作成便于携带的封装代码单元。
其它例子
??? 闭包最著名的应用之一可能是Douglas Crockford的《在ECMAScript对象中模拟私有实例变量的技术》。它可以被扩展到各种基于作用域嵌套结构的访问控制/可见性控制中,包
括《为ECMAScript对象模拟私有静态成员》。
??? 闭包可能的应用无穷无尽,理解它们是如何运作的可能是认识它们用途的最好指南。
意外的闭包
??? 把任何函数渲染到在创建它们的函数体之外可以访问会形成一个闭包。这导致闭包非常容易被创建,并且后果之一就是不熟悉闭包作为语言特性的javascript作者可能会注意到内部函数的各种作用并且运用内部函数,但是没有考虑结果、没有注意到闭包被创建或者这样做的影响是什么。
??? 意外地创建闭包可能会产生有害的副作用,就像后文对IE内存泄漏问题的描述中所述,但它们也可以影响代码效率。这不是闭包自身的问题,小心地利用它们确实可以有助于创建高效的代码。这就是使用内部函数对效率的影响。
??? 常见的情况就是内部函数被用做DOM元素的事件处理函数。例如,以下代码可能被用来为链接元素添加一个onclick处理函数:
/*
??? 定义全局变量,它持有添加到链接href上的值,它作为以下函数的查询字符串:
*/
var quantaty = 5;
/*
??? 当一个链接被传递给这个函数时(调用函数时作为linkRef参数),一个onclick事件处??? 理器就被添加到了链接上,这将把全局变量quantaty的值添加到链接的href属性上作??? 为查询字符串,然后返回true,这样链接将会导航到href指定的资源,此时所分配的??? 查询字符串将会被包含在内:
*/
function addGlobalQueryOnClick(linkRef){
/*
??? ??? 如果linkRef参数的类型可以被转换成true(如果它引用了一个对象):
??? */
??? if(linkRef){
??????? /*
??? ??? ??? 执行一个函数表达式,给link元素的onclick处理器分配一个
??? ??? ??? 引用,引用执行函数表达式时创建的函数对象:
??????? */
??????? linkRef.onclick = function(){
??????????? /*
??? ??? ??? ??? 这个内部函数表达式把查询字符串添加到元素的href属性上,
??? ??? ??? ??? 此函数被绑定为一个事件处理器:
??????????? */
??????????? this.href += ('?quantaty='+escape(quantaty));
??????????? return true;
??????? };
??? }
}
??? 无论addGlobalQueryOnClick函数何时被调用,一个新的内部函数都会被创建(并且通过它的赋值产生一个闭包)。从效率角度考虑,如果addGlobalQueryOnClick函数仅仅调用一次两次,这影响不大,但是如果这个函数被大规模应用,将会创建大量独立的函数对象(每次执行内部函数表达式都会创建一个)。
??? 以上代码没有利用的一个事实是,内部函数在被创建的函数之外可以访问(或者说输出闭包)。结果是,通过单独定义一个函数作为事件事件处理器,然后分配一个引用给这个函数的事件处理器的属性,可以达到同样的效果。只有一个函数对象会被创建,并且使用事件处理函数的所有元素都会共享这唯一一个函数的引用:
/*
定义全局变量,它持有添加到链接href上的值,它作为以下函数的查询字符串:
*/
var quantaty = 5;
/*
??? 当一个链接被传递给这个函数时(调用函数时作为linkRef参数),一个onclick事件处??? 理器就被添加到了链接上,这将把全局变量quantaty的值添加到链接的href属性上??? 作为查询字符串,然后返回true,这样链接将会导航到href指定的资源,此时分??? 配??? 的查询字符串将会被包含在内:
*/
function addGlobalQueryOnClick(linkRef){
??? /*如果linkRef参数的类型可以被转换成true(如果它引用了一个对象):*/
??? if(linkRef){
??????? /*
??? ??? ??? 执行一个函数表达式,给link元素的onclick处理器分配一个
??? ??? ??? 引用,引用执行函数表达式时创建的函数对象:
??????? */
??????? linkRef.onclick = forAddQueryOnClick;
??? }
}
/*
??? 一个全局函数声明,这个函数被设计用来作为链接元素的事件处理器,添加
??? 一个全局变量的值给元素的href属性作为一个事件处理器:
*/
function forAddQueryOnClick(){
??? this.href += ('?quantaty='+escape(quantaty));
??? return true;
}
??? 因为第一个版本中的内部函数没有利用它自己产生的闭包,不使用内部函数会更有效,并且这样不会重复创建本质上相同函数对象。
??? 一个类似的考虑适用于对象的构造函数。这种情况并不少见,参见以下的代码框架:
function ExampleConst(param){
/*
通过执行函数表达式创建对象的方法,并将结果函数对象的引用赋值给
所创建对象的属性:
??? */
??? this.method1 = function(){
??????? ... // method body.
??? };
??? this.method2 = function(){
??????? ... // method body.
??? };
??? this.method3 = function(){
??????? ... // method body.
??? };
/* 将构造函数的参数赋值给对象的属性:*/
??? this.publicProp = param;
}
??? 每次这个构造函数被用来创建一个对象,使用new ExampleConst(n)时,一组新的函数对象就被创建用来作为它的方法。所以,对象创建得越多,随着它们一起被创建的函数对象就越多。
??? Douglas Crockford的在javascript对象上模拟私有成员的技术利用了返回闭包的形式,把内部函数的引用赋给公共属性,这个属性是所创建的对象在构造函数内部创建的。但是如果一个对象的方法没有利用闭包,初始化每个对象时会创建多个函数对象,这使得初始化过程变慢,并且将会消耗更多的资源用来容纳这些创建的额外函数对象。
??? 在这种情况下,创建函数对象一次,然后把它们的引用赋值给构造函数的prototype会更有效,这样它们可以被使用这个构造函数创建的所有对象共享:
function ExampleConst(param){
??? /*将构造函数的参数赋值给对象的一个属性:*/
??? this.publicProp = param;
}
/*
通过执行函数表达式创建对象的方法,并将结果函数对象的引用赋值
给所构造函数的prototype属性:
*/
ExampleConst.prototype.method1 = function(){
??? ... // method body.
};
ExampleConst.prototype.method2 = function(){
??? ... // method body.
};
ExampleConst.prototype.method3 = function(){
??? ... // method body.
};

IE中的内存泄漏问题
??? IE浏览器(验证了版本4到6(目前写作时是版本6))在其垃圾收集系统中存在一个错误,它阻止了对ECMAScript和一些宿主对象的垃圾收集,如果这些宿主对象是形成“循环引用”的一部分。出问题的宿主对象是DOM节点(包括document对象以及它的后代)和ActiveX对象。如果形成的循环引用中存在它们中的一个或者多个,则这些对象都不会被释放直到浏览器被关闭,并且它们消耗的内存系统都无法使用直到浏览器关闭。
??? 循环引用是两个或者多个对象互相应用,用一种互相连接并能回到起点的方式引用。例如对象1有一个属性引用对象2,对象2有一个属性引用对象3,然后对象3有一个属性回到对象1。对纯粹的ECMAScript对象来说,只要没有其它对象引用对象1,2或者3中的其中之一,它们互相引用对方的事实就能被确定,并且它们对垃圾收集有效。但是在IE中,如果这些对象中的任意一个恰好是DOM节点或者ActiveX对象,则垃圾收集器无法发现它们之间的这种循环关系是和系统的其它部分相隔离的并释放它们。事实上它们都驻留在内存中直到浏览器被关闭。
??? 闭包极其容易形成循环引用。如果一个函数对象形成了的一个闭包,例如,被分配为一个DOM节点的事件处理器,到这个节点的引用就被分配给了执行环境中的活动对象/可变对象之一,然后循环引用就形成了。DOM_Node.onevent -> function_object.[[scope]] -> scope_chain -> Activation_object.nodeRef -> DOM_Node。这非常容易做到,浏览一个在每个页面的一段代码中形成了这种引用关系的站点可能会消耗掉系统的大部分内存(可能是全部)。
??? 可以小心避免形成循环引用,实在无法避免时可以采取补救措施,例如使用IE的onunload事件,将事件处理函数的引用赋值为null。认识这个问题并理解闭包(及其机制)是在IE中避免此问题的关键所在。

comp.lang.javascript FAQ notes T.O.C.
?? Written by Richard Cornford. March 2004.

?? With corrections and suggestions by:-
o??? Martin Honnen.
o??? Yann-Erwan Perio (Yep).
o??? Lasse Reichstein Nielsen. (definition of closure)
o??? Mike Scirocco.
o??? Dr John Stockton.
o??? Garrett Smith.

热点排行