Javascript Closure[2]

Closures (闭包)


垃圾回收机制 (Garbage Collection)

ECMAScript使用自动垃圾回收机制,规范中并没有定义垃圾回收的细节,而是把具体实现留给了各浏览器厂商,比如有一些实现赋予垃圾回收器非常低的优先级(可能导致内存泄漏)。一般而言默认的原则是一个无法被引用了的对象,即所有对它的引用都变得不可访问的时候,它就变成了一个可回收的对象并且在将来的某个时间会被销毁,它占用的一切资源也可以释放。

 

垃圾回收典型的触发时机是当一个执行上下文环境退出时,作用域链结构、Activation Object和所有在执行上下文环境中创建的对象,包括函数对象,都将变得不可访问。所以,对垃圾收集器来说它们都可回收。

 

形成闭包

闭包是这样形成的:在Outer函数调用时产生执行上下文环境,Outer函数中存在一个Inner函数,内部函数对象的引用作为返回值退出了Outer函数,被赋给了另外一个对象的某个属性或者某全局变量。例:

function exampleClosureForm(arg1, arg2){
    var localVar = 8;
    function exampleReturned(innerArg){
        return (arg1+arg2)/(innerArg+localVar);
    }
    return exampleReturned;
}
var globalVar = exampleClosuresForm(2,4);

现在对于这次调用创建的执行上下文来说,内部创建的函数对象无法被垃圾收集器回收,因为它仍然处于可访问状态,可以通过globalVar(5)来触发它。然而现在有更复杂的事情发生了,就是globalVar引用的函数对象自身带有一个作用域链属性,包含着一个Activation Object元素,这个Activation Object是上一次exampleClosureForm被调用时候在它的函数执行上下文中创建的。现在这个Activation Object也无法被垃圾回收器回收因为每次调用globalVar引用的函数,都需要把这个函数对象的作用域链添加到调用函数上下文的作用域链上(Activation Object在函数对象的作用域链上)

 

由于globalVar引用了Inner Function对象,导致当初那个Outer Function调用产生的Activation Object关联着上了Inner函数对象的作用域链而陷入一个无法回收的封闭空间里,这就是闭包。并且,这个Activation Object和函数上下文的作用域链上的变量都还保留着当初调用Outer函数时的状态。当globalVar被调用而产生的作用域链解析过程中,Inner函数可以在遍历中找到那些变量就好像它们就是自身的属性一样。这些变量依然可以读/写,哪怕是本次调用产生的函数上下文退出之后。

 

假设这样调用:globalVar(2)

首先一个新的函数上下文环境会产生,并且随之而来的是一个属于这个函数上下文的Activation Object(我们将称之为ActInner1),它将会被挂载到上下文作用域链的首个位置,所以本次调用形成的作用域链为ActInner -> Outer(之前调用exampleClosuresForm产生) -> global object。而标识的解析会和作用域链相反,所以是全局对象域的属性变量首先识别,之后再是Outer域最后才是ActInner域。

 

这就是Js如何解析和识别变量,并通过Inner函数来形成闭包的。闭包会保留对一些域变量的引用使得它们无法被回收,仍然处于可读/写状态。直到所有的Inner函数调用都完毕,才能使得Inner函数对象本身可以被回收,那些闭包保留下来的变量占据的内存也才能得意释放。

 

内部函数也可以含有更内层函数,并且再内层的函数也可以从函数执行上下文中返回来形成一个对自身的闭包,以至于每次嵌套,当前作用域链都会意外获得相对当前的内部函数执行声明代码上下文中的Activation Object。由于这种不确定因素,大家都不太愿意去在编码中使用嵌套函数。

 

闭包可以做什么?

开个玩笑的回答就是闭包可以实现任何需求。有人曾径告诉我闭包使得Javascript可以模仿任何东西,唯一的局限成了去构思我们要如何实现这种模仿。这样说有一点费解,不如来看一些更实际实践化的东西。

 

Example-1 setTimeout

一个典型的闭包应用就是通过传递参数改变函数执行的优先级。例如当一个函数做为第一个参数传递给setTimeout函数时。setTimeout创建了一个函数执行的schedule,或者说一个可执行的Js代码串。如果一小块代码希望通过setTimeout执行就需要封装在一个匿名函数里并传递给setTimeout的第一个参数。其它时候都是直接传递一个函数对象的引用,这种函数引用传递就无法携带参数了。尽管如此,如果返回一个嵌套的Inner函数给setTimeoutInner函数声明的Outer函数携带的参数依然可以在setTimeout执行上下文中使用。见一个例子:

function callLater(paramA,paramB,paramC){
    return (function(){
        paramA [paramB] = paramC;
    });
}
var funcRef = callLater(elStyle,”display”,”none”);
hideMenu = setTimeout(funcRef, 500);

Example-2根据对象实例方法关联函数

除了setTimeout还有多种情况通过引用使得一个函数在未来的某个时间点执行,此时向执行函数传递参数变得不是那么容易。一个例子是当一个Js对象被设计做为一个DOM元素的封装时,它拥有doOnClickdoMouuseOver、和doMouseOut等方法,当这些方法被调用时由event对象统一触发对应的DOM元素,但是可以存在任意数量的对象实例于不同的DOM元素关联,每一个对象并不知道自身如何被初始化、如何在全局环境中被引用,因为它们不知道全局作用域中哪一个引用指向自身。问题来了,一个event对象处理函数关联上了一个Javascript对象,它如何知道这个对象的哪一个方法将要被调用呢?

 

下面的例子使用了一个小型通用的闭包模式,关联一个对象和对象事件处理函数、调度event handler来执行对象特定的方法,传递事件对象和元素对象的引用,并返回方法调用的结果(返回值)

function associateObjectWithEvent(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 = associateObjectWithEvent(this,”doOnClick”);
        el.onmouseover = associateObjectWithEvent(this,”doMouseOver”);
        el.onmouseout = associateObjectWIthEvent(this,”doMouseOut”);
        //……
    }
}
DhtmlObject.prototype .doOnClick = function(event, element){…};
DhtmlObject.prototype.doMouseOver = function(event, element){…};
DhtmlObject.prototype.doMouseOut = function(event,element){…};

现在所有DhtmlObject实例都可以关联任意它们感兴趣的DOM元素,并不需要知道对元素的操作是如何被代码引入的,也不会影响全局命名空间和与其它对象的冲突。

Example-3:封装相关联的函数

闭包可以被用来创建附加的作用域对象来把相关联和依赖的代码归组,这样可以减少意外交互。设想有一个函数需要创建一个字符串并避免频繁的进行字符串的拼接,于是期望使用Array来存储子串并使用Array.prototyper.join方法来返回最终完整字符串。这个数组对象就相当于一个输出缓冲区,但是被定义为函数的local变量将会导致每次调用函数都会重新创建一个Array,在只需要改变数组内容的需求下这将是完全没有必要的。

 

一种解决方案是把数组设置为全局变量这样就可以被重用而不需要每次重新构造。但这样做会导致一个问题,array做为全局变量,然后又增加一个全局变量引用那个把array当作Buffer的函数对象会导致array再一次被这个全局引用的某个属性所引用(通过解析函数声明产生的property)。这样使得代码管理上会更加繁琐困难,使用者必须同时记住有全局就两个位置都包含了对这个array的引用。同时也会使得代码集成起来困难重重,因为本来只需要确认全局变量名字是唯一的就可以了,现在还要增加确认这个全局变量array依赖的函数名是否是唯一的(与其他namespace代码集成时,命名唯一是很重要的)

 

闭包允许这个缓冲数组关联上一个函数,并且同步的保持这个数组的属性名字不在全局作用域中,解除了名字冲突的可能性。这个戏法的关键是这里通过执行一个函数表达式,再创建一个执行上下文并且返回一个内部函数,返回的内部函数也就是被外部代码所能使用的。然后那个buffer array就做为一个被执行函数表达式的local变量一直存在下去。(只需要调用一次,就可以保持这个buffer array对象的一直存在)

 

接下来的一个例子将会创建一个HTML串,大多数串内容都是不可改变的,但是这个HTML串在函数调用时会被以参数形式传递进来的变量所修饰点缀。一个内部函数的引用将会从函数执行上下文中返回出来并且赋值给一个全局变量,所以看起来就像一个全局函数一样。那么buffer array就被定义为Outer函数的一个local变量,它并不会暴露在全局作用域中,也不需要在每次调用时被重新创建。

var getImgInPositionedDivHtml = (function(){
    var buffArr = [‘<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(‘’); 
    });
})();

如果有一个函数希望依赖与另外一个或者几个函数,但是那些函数都不希望被直接的导入,就可以利用这个例子相似的技术。使用这种技术可以方便创建一个可移植(无命名冲突)的,封装良好的,包含多个复杂函数的流程。另外的例子就是Douglas Crockford的私有变量模拟技术,利用内部函数提供对变量的访问权限,可以扩展模拟出任何结构来,例如private static成员,总之闭包的可能性是无限大的,理解它如何工作会给实际编码带来最良好的指导。

 

意外的闭包

把内部函数的导向Outer函数体外会导致闭包。这将使得闭包非常容易创建出来。所以Javascript的编写者可能自己都不知情的情况下就在代码中产生了闭包。意外的产生闭包将会带来危害,比如接下来的这个例子:IE内存泄露问题。闭包也会影响代码的性能,其实这并不是闭包本身的问题,小心谨慎的使用闭包能带来性能的提升(本质上就是使用Inner函数)。常见的例子是Inner函数被用于DOM元素的事件处理。

var quantity = 5;
function addGlobalQueryOnClick(linkRef){
    if(linkRef){
    linkRef.onclick = function(){
        this.href += (‘?quantaty=’+escapes(quantaty));
        return true;
      }
    }
}

addGlobalQueryOnClick函数仅仅被调用一次两次的时候不会有什么问题,但是当它频繁被调用的时候就会创建许多不同的函数对象就会被创建出来。其主要原因是内部函数被返回到了Outer函数外部一个全局引用的属性上,由此形成了闭包。如果把事件处理函数做为引用独立出来,可以达到同样的目的,却避免了闭包的形成(只有一个函数对象会被创建出来并且被所有的事件处理函数引用将会指向同一个函数对象)

linkRef = forAddQueryOnClick;

 

另外一个类似的例子是对象的构造函数,下面代码不常使用:

function ExampleConst(param){
    this.method1=function(){ … };
    this.method2=function(){ … };
    this.method3=function(){ … };
    this.publicProp = param;
}

每一次通过new ExampleConst(n)使构造函数被调用,都会产生一个函数对象集合,多个对象构造会产生大量的函数对象集合。这样初始化过程将会变慢,更多的资源被浪费。所以这种情况我们习惯把成员函数定义到函数的原型链中去,因为原型对象只会随着函数对象的创建被创建初始化一次。

Example.prototype.method1= function(){ …};

Example.prototype.method2= function(){ …};

 

题外话—IE内存泄露问题

IE浏览器从IE4IE6的垃圾回收器都存在一种错误的回收策略,就是当一些可回收对象互相之间形成循环引用的时候,即使它们都不再使用了也不会回收。这些对象可以是任何的DOM元素,包括document对象本身和它的所有子孙、还有ActiveX对象。一旦循环引用形成,它们中的任何一个都无法被回收直到浏览器关闭。

 

举个例子,obj1有一个属性指向obj2obj2有个属性指向obj3,而obj3又有个属性指向obj1,这时候就形成了循环引用。如果它们都是纯ECMAScript对象,在没有其他对象指向obj1obj2obj3中的任何一个时,它们整体会被隔离出来并且对垃圾回收器而言处于可回收状态。但在IE浏览器上,如果任何一个正好是一个DOM元素结点或者ActiveX对象,垃圾回收器就无法侦测到这种循环关系而把它们隔离出来,以至于它们的回收就必须等到浏览器关闭(一直停留在内存中)

 

闭包是形成循环引用的罪魁祸首(开个玩笑)。如果形成闭包的函数对象被赋值给一个DOM元素结点属性上,并且这个DOM元素结点又挂在了函数执行的Activation Object上,循环引用就形成了。DOM_Node.onevent->function_object.[scope]->scope_chain->Activation_object.nodeRef->DOM_Node。浏览一些页面形成这种循环引用之后,浏览器内存可能会占用整个系统内存。

 

【End】

 原文地址:http://jibbering.com/faq/faq_notes/closures.html

 

你可能感兴趣的:(JavaScript,function,object,浏览器,buffer,Closures)