Minakata的博客

世界上没有游戏制造机,有的只是艰辛的劳动。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

javascript核心知识整理之一:函数闭包

Posted on 2011-10-05 23:54  Minakata  阅读(339)  评论(0编辑  收藏  举报

调用对象

当javascript解释器调用一个函数,它首先将作用域设置为定义函数的时候起作用的那个作用域链。接下来,它在作用域的前面添加一个新的对象,这叫做调用对象。调用对象用一个名为arguments的属性来初始化,这个属性引用了函数的Arguments对象。函数的命名的参数添加到调用对象的后面。用var语句声明的任何局部变量也都定义在这个对象中。既然这个调用对象位于作用域链前端,局部变量、函数参数以及Arguments对象都在函数内的作用域中。当然,这也意味着它们隐藏了作用域链更上层的任何同名的属性。

函数作用域和闭包

词法作用域:javascript中的函数是通过词法来划分作用域的,而不是动态地划分作用域的。这意味着,它们在定义它们的作用域里进行,而不是在执行它们的作用域里进行。当定义了一个函数,当前的作用域链就保存起来,并且成为函数的内部状态的一部分。在最顶级,作用域链仅由全局对象组成,而并不和词法作用域相关。然而,当定义一个嵌套的函数时,作用域链就包括外围的包含函数。这意味着嵌套的函数可以访问包含函数的所有参数和局部变量。注意:尽管当一个函数定义了的时候,作用域链就固定了,但作用域链中定义的属性还没有固定。作用域链是“活的”,并且函数在被调用的时候可以访问任何当前的绑定。

作为闭包的嵌入函数

javascript允许嵌入的函数,允许函数用作数据,并且使用词法作用域,这些因素相互交互,创造了惊人的和强大的效果。为了说用这一点,考虑一个定义在函数f中的函数g。当f被调用的时候,作用域链包含了对f的这一调用的调用对象,后边是全局对象。g定义在f中,因此,这个作用域链保存为g的定义的一部分。当g被调用的时候,作用域链包括3个对象:它自己的调用对象,f的调用对象以及全局对象。当嵌入的函数在它们定义的同一个词法作用域里调用的时候,它们是很好理解的。如下所示,代码不会有任何惊人之处:

var x = "golbal";
function f(){
var x = "local";
function g() { alert(x); }
g();
}
f(); //Calling this function displays "local"

然而,在javascript中,函数和其他值一样,也是数据,因此,它们可以从函数返回,它们可以从函数返回,被赋给对象属性,存储在数组中,等等。除非涉及嵌入的函数,这也不会导致什么令人吃惊的事情。考虑如下代码,其中包含了一个函数,它返回一个嵌套的函数。每次调用这个函数,它都返回一个函数。返回的函数的javascript代码总是相同的,但是,它所创建的作用域略为不同,因为外围函数的参数值在每次调用中都不同(也就是说,外围函数的每次调用的作用域链上,有一个不同的调用对象)。如果把返回的函数存储在一个数组中国,然后来调用其中的每一个,将会看到每次返回一个不同的值。既然每个函数包含同样的javascript代码,并且每段代码都是从相同的作用域调用,那么,唯一可能导致不同返回值得因素就是函数定义所在的作用域:

//This function returns a function each time it is called
//
The scope in which the function is defined differs for each call
function makefunc(x){
return function(){return x;}
}

//Call makefunc() several times,and save the results in an array;
var a = {makefunc(0),makefunc(1),makefunc(2)};

//Now call these functions and display their values.
//
Although the body of each function is the same,the scope is different,
//and each call returns a different value:

alert(a[0]());//displays 0
alert(a[1]());//displays 1
alert(a[2]());//displays 2

这段代码的结果正是可以从严格应用此法作用域规则所期待的,函数在它所定义的作用域中执行。然而,这些结果令人吃惊的原因是,当定义了局部作用域的函数退出的时候,期待局部作用域能够终止并退出。也就是说,实际上这正是通常所发生的情况。当一个函数被调用的时候,就为它创建了一个调用对象并放置到作用域链中。当函数退出的时候,调用对象也从作用域链中移除。当没有涉及嵌套的函数的时候,作用域链式对调用对象的唯一的引用。当对象从链中移除了,也就没有对它的引用了,最终通过对它的垃圾收集而完成。

但是,嵌套的函数改变了这一情景。如果创建了一个嵌套的函数,这个函数的定义引用了调用对象,因为调用对象在这个函数所定义的作用域链的顶端。可是,如果嵌套的函数只是在外围函数的内部使用,那么,对嵌套函数的唯一的引用在调用对象之中。当外围函数返回的时候,嵌套的函数引用了调用对象,并且调用对象引用了嵌套的函数,但是,没有其他的东西引用他们二者,因此,对这两个对象都可以进行垃圾收集了。

如果把嵌套的函数的引用保存到一个全局作用域中,情况又不相同了。使用嵌套的函数作为外围函数的返回值,或者把嵌套的函数存储为某个其他对象的属性来做到这一点。在这种情况下,有一个对嵌套的函数的外部引用,并且嵌套的函数将它的引用保留给外围函数的调用对象。结果是,外围函数的一次特定调用的调用对象依然存在,函数的参数和局部变量的名字和值在这个对象中得以维持。javascript代码不会以任何方式直接访问调用对象,但是,他所定义的属性是对嵌入函数任何调用的作用域链的一部分。(注意,如果一个外围函数存储了两个嵌套函数的全局引用,这两个嵌入函数共享同一个调用对象,并且,一个函数的一次调用所做出得改变对于另一个函数的调用来说也是可见的。)javascript函数时将要执行的代码以及执行这些代码的作用域构成的一个综合体。在计算机科学术语里,这种代码和作用域的综合体叫做闭包。

以上文字出自《javascript权威指南》,读起来感觉很晦涩难懂。下面用稍微通俗点的语言解释一下:

匿名函数最有趣的用途是用来创建闭包。闭包(closure)是一个受到保护的变量空间,由内嵌函数生成。javascript具有函数级的作用域。这意味着定义在函数内部的变量在函数外部不能被访问。javascript的作用域又是词法性质的。这意味着函数运行在定义它的作用域中,而不是在调用它的作用域中。把这两个因素结合起来,就能通过把变量包裹在匿名函数中而对其加以保护。请看以下两段代码,输出结果是一样的。

var f = (function(){ 
var id = 0;
return function(){return id++;};
})();

document.write(f());//display 0
document.write(f());//display 1
document.write(f());//display 2
document.write(f());//display 3
document.write(f());//display 4

var f;
(function(){
var id = 0;
f = function(){return id++;};
})();

document.write(f());//display 0
document.write(f());//display 1
document.write(f());//display 2
document.write(f());//display 3
document.write(f());//display 4