闭包

坑军之王的头像
坑军之王
171

函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在作用域中。这就是闭包。函数变量可以被隐藏与作用域链内,看起来像是函数将变量“包裹”起来。

在mozilla开发者平台上对闭包的定义是这样的:

一个函数和对其周围状态lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。

从技术角度可以这么说:所有的JavaScript函数都是闭包:他们都是对象,他们都关联到作用域链。定义大多数函数时的作用域链在调用函数时依然有效,但这并不影响闭包。当调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链时,事情就变得非常微妙。当一个函数嵌套了另一个函数,外部函数将嵌套的函数对象作为返回值返回的时候往往会发生这种事情。

在这里我们使用一个例子来了解以下嵌套函数的作用域规则:

var scope = “global scope”; //全局变量
function checkscope(){
var scope =”local scope”//局部变量
function f(){return scope;}//在作用域中返回这个值
return f();
}
checkscope();				//local scope

checkscope()函数声明了一个局部变量,并定义了一个函数f(),函数f()返回了这个变量的值,最后将函数f()的执行结果返回。当引用checkscope()的时候返回值是什么是显而易见的。我们将代码进行一点小小的改动。

var scope = “global scope”; //全局变量
function checkscope(){
var scope =”local scope”//局部变量
function f(){return scope;}//在作用域中返回这个值
return f;
}
checkscope()();				//?

在这段代码中,我们将函数内的一对圆括号移动到checkscope()之后。Checkscope()现在只返回函数内嵌套的一个函数对象,而不是直接返回结果。在定义函数的作用域外面,调用这个嵌套的函数(包括最后一行代码的最后一对圆括号),试着预测一下结果。

首先回想一下词法作用域的基本规则:JavaScript函数的执行用到了作用域链,这个作用域链是函数定义的时候创建的。嵌套的函数f()定义在这个作用域链内,其中变量scope一定是局部变量,无论何时执行函数f(),这种绑定在执行f()时依然有效。因此最后一行代码返回“local scope”,而不是“gobal scope”。简单来说,闭包的这个特性可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义他们的外部函数。

实现闭包

如果理解了词法作用域的规则,那对于闭包的理解就锦上添花了:函数定义时的作用域链到函数执行时依然有效。然而部分程序员很难理解,他们在理解闭包实现细节上自己把自己给绕晕了:外部函数定义的局部变量在函数返回后就不存在了。

再想想作用域链的定义:我们将作用域链描述为一个对象列表。它不是绑定的栈,每次调用JavaScript函数的时候,都会为之创建一个新的对象来保存局部变量,把这个对象添加到作用域链中。当函数返回的时候,就从作用域链中将这个绑定变量的对象删除。如果不存在嵌套的函数,也没有其他引用指向这个绑定对象,它就会被当成垃圾回收掉。如果定义了嵌套的函数,每个嵌套的函数都各自对应一个作用域链,且这个作用域链只想一个变量绑定对象。但如果这些嵌套的函数对象在外部函数中保存下来,那么它们也会和所指向的变量绑定对象一样当做垃圾回收。但是如果这个函数定义了嵌套的函数,并将它作为返回值返回或者存储在某处的属性里,这就会有一个外部引用指向这个嵌套的函数。它就不会被当作垃圾回收,并且它所指向的变量绑定对象也不会被当作垃圾回收。

在使用闭包的时候,要小心,因为闭包很容易造成“循环引用”。当DOM对象和JavaScript对象这件存在循环引用的时候需要格外小心,在部分浏览器里会造成内存泄露。

 

像counter一样的私有变量不是只能用在单独的闭包内,在同一个外部函数内定义的多个嵌套函数可以访问它,这多个嵌套函数都能共享一个作用域,看看如下代码:

function counter(){
var n=0;
return{
count:function(){return n++;}
reset:function(){n=0;}
};
}
var c =counter(),d=counter();
c.count()					//0
d.count()					//0
c.reset()					//reset()方法和count()方法共享状态
c.count()					//0
d.count()					//1

counter()函数返回了一个“计数器”对象,这个对象包含两种方法:count()返回下一个整数,reset()将计数器重置为内部状态。首先要理解,这两个方法都可以访问私有变量n。在这,每次调用counter()都会创建一个新的作用域链和一个新的私有变量。因此,如果调用counter()两次,则会得到两个计数器对象,而且彼此包含不同的私有变量,调用其中一个计数器对象的count()和reset()不会影响到另外一个对象。

用户评论
评论列表