从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链。定义大多数函数时的作用域链在调用函数时依然有效,但这并不影响闭包。当调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链时,事情就变得非常微妙。当一个函数嵌套了另外一个函数,外部函数将嵌套的函数对象作为返回值返回的时候往往会发生这种事情。有很多强大的编程技术都利用到了这类嵌套的函数闭包,以至于这种编程模式在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()会返回"local scope"。现在我们对这段代码做一点改动。你知道这段代码返回什么吗?
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",而不是"global scope"。简言之,闭包的这个特性强大到让人吃惊:它们可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义它们的外部函数。
实现闭包
我们将作用域链描述为一个对象列表,不是绑定的栈。每次调用JavaScript函数的时候,都会为之创建一个新的对象用来保存局部变量,把这个对象添加至作用域链中。当函数返回的时候,就从作用域链中将这个绑定变量的对象删除。如果不存在嵌套的函数,也没有其他引用指向这个绑定对象,它就会被当做垃圾回收掉。如果定义了嵌套的函数,每个嵌套的函数都各自对应一个作用域链,并且这个作用域链指向一个变量绑定对象。但如果这些嵌套的函数对象在外部函数中保存下来,那么它们也会和所指向的变量绑定对象一样当做垃圾回收。但是如果这个函数定义了嵌套的函数,并将它作为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的函数。它就不会被当做垃圾回收,并且它所指向的变量绑定对象也不会被当做垃圾回收.
uniqueInteger()函数,这个函数使用自身的一个属性来保存每次返回的值,以便每次调用都能跟踪上次的返回值。但这种做法有一个问题,就是恶意代码可能将计数器重置或者把一个非整数赋值给它,导致uniquenterger()函数不一定能产生“唯一”的“整数”。而闭包可以捕捉到单个函数调用的局部变量,并将这些局部变量用做私有状态。我们可以利用闭包这样来重写uniqueInteger()函数:
var uniqueInteger=(function(){//定义函数并立即调用
var counter=0;//函数的私有状态
return function(){return counter++;};
}());
你需要仔细阅读这段代码才能理解其含义。粗略来看,第一行代码看起来像将函数赋值给一个变量uniqueInteger,实际上,这段代码定义了一个立即调用的函数(函数的开始带有左圆括号),因此是这个函数的返回值赋值给变量uniqueInteger。现在,我们来看函数体,这个函数返回另外一个函数,这是一个嵌套的函数,我们将它赋值给变量uniqueInteger,嵌套的函数是可以访问作用域内的变量的,而且可以访问外部函数中定义的counter变量。当外部函数返回之后,其他任何代码都无法访问counter变量,只有内部的函数才能访问到它。
像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:因为我们重置了c
d.count()//=>1:而没有重置d
counter()函数返回了一个“计数器”对象,这个对象包含两个方法:count()返回下一个整数,reset()将计数器重置为内部状态。首先要理解,这两个方法都可以访问私有变量n。再者,每次调用counter()都会创建一个新的作用域链和一个新的私有变量。因此,如果调用counter()两次,则会得到两个计数器对象,而且彼此包含不同的私有变量,调用其中一个计数器对象的count()或reset()不会影响到另外一个对象。
从技术角度看,其实可以将这个闭包合并为属性存取器方法getter和setter。下面这段代码所示的counter()函数的版本是6.6节中代码的变种,所不同的是,这里私有状态的实现是利用了闭包,而不是利用普通的对象属性来实现:
function counter(n){//函数参数n是一个私有变量
return{//属性getter方法返回并给私有计数器var递增1
get count(){return n++;},//属性setter不允许n递减
set count(m){
if(m>=n)n=m;
else throw Error("count can only be set to a larger value");
}
};
}
var c=counter(1000);
c.count//=>1000
c.count//=>1001
c.count=2000
c.count//=>2000
c.count=2000//=>Error!
需要注意的是,这个版本的counter()函数并未声明局部变量,而只是使用参数n来保存私有状态,属性存取器方法可以访问n。这样的话,调用counter()的函数就可以指定私有变量的初始值了。
使用闭包技术来共享的私有状态的通用做法。这个例子定义了addPrivateProperty()函数,这个函数定义了一个私有变量,以及两个嵌套的函数用来获取和设置这个私有变量的值。它将这些嵌套函数添加为所指定对象的方法:
利用闭包实现的私有属性存取器方法
//这个函数给对象o增加了属性存取器方法
//方法名称为get<name>和set<name>。如果提供了一个判定函数
//setter方法就会用它来检测参数的合法性,然后在存储它
//如果判定函数返回false,setter方法抛出一个异常
//
//这个函数有一个非同寻常之处,就是getter和setter函数
//所操作的属性值并没有存储在对象o中
//相反,这个值仅仅是保存在函数中的局部变量中
//getter和setter方法同样是局部函数,因此可以访问这个局部变量
//也就是说,对于两个存取器方法来说这个变量是私有的
//没有办法绕过存取器方法来设置或修改这个值
function addPrivateProperty(o,name,predicate){
var value;//这是一个属性值
//getter方法简单地将其返回
o["get"+name]=function(){return value;};//setter方法首先检查值是否合法,若不合法就抛出异常
//否则就将其存储起来
o["set"+name]=function(v){
if(predicate&&!predicate(v))
throw Error("set"+name+":invalid value"+v);
else
value=v;
};
}
//下面的代码展示了addPrivateProperty()方法
var o={};//设置一个空对象
//增加属性存取器方法getName()和setName()
//确保只允许字符串值
addPrivateProperty(o,"Name",function(x){return typeof x=="string";});
o.setName("Frank");//设置属性值
console.log(o.getName());//得到属性值
o.setName(0);//试图设置一个错误类型的值
我们已经给出了很多例子,在同一个作用域链中定义两个闭包,这两个闭包共享同样的私有变量或变量。这是一种非常重要的技术,但还是要特别小心那些不希望共享的变量往往不经意间共享给了其他的闭包,了解这一点也很重要。看一下下面这段代码:
//这个函数返回一个总是返回v的函数
function constfunc(v){return function(){return v;};}//创建一个数组用来存储常数函数
var funcs=[];
for(var i=0;i<10;i++)funcs[i]=constfunc(i);//在第5个位置的元素所表示的函数返回值为5
funcs[5]()//=>5
这段代码利用循环创建了很多个闭包,当写类似这种代码的时候往往会犯一个错误:那就是试图将循环代码移入定义这个闭包的函数之内,看一下这段代码:
//返回一个函数组成的数组,它们的返回值是0~9
function constfuncs(){
var funcs=[];
for(var i=0;i<10;i++)
funcs[i]=function(){return i;};
return funcs;
}
var funcs=constfuncs();
funcs[5]()//返回值是什么?
上面这段代码创建了10个闭包,并将它们存储到一个数组中。这些闭包都是在同一个函数调用中定义的,因此它们可以共享变量i。当constfuncs()返回时,变量i的值是10,所有的闭包都共享这一个值,因此,数组中的函数的返回值都是同一个值,这不是我们想要的结果。关联到闭包的作用域链都是“活动的”,记住这一点非常重要。嵌套的函数不会将作用域内的私有成员复制一份,也不会对所绑定的变量生成静态快照(static snapshot)。
注意
书写闭包的时候还需注意一件事情,this是JavaScript的关键字,而不是变量。正如之前讨论的,每个函数调用都包含一个this值,如果闭包在外部函数里是无法访问this的,除非外部函数将this转存为一个变量:
var self=this;//将this保存至一个变量中,以便嵌套的函数能够访问它
绑定arguments的问题与之类似。arguments并不是一个关键字,但在调用每个函数时都会自动声明它,由于闭包具有自己所绑定的arguments,因此闭包内无法直接访问外部函数的参数数组,除非外部函数将参数数组保存到另外一个变量中:
var outerArguments=arguments;//保存起来以便嵌套的函数能使用它
利用了这种编程技巧来定义闭包,以便在闭包中可以访问外部函数的this和arguments值。