在经历了漫长的作用域链、调用对象和词法作用域之后,终于可以讨论闭包了。
对于闭包这个概念,有各种各样的解释。我比较喜欢的是闭包是《Java 理论与实践: 闭包之争》中的定义:可以包含自由(未绑定)变量的代码块;这些变量不是在这个代码块或者任何全局上下文中定义的,而是在定义代码块的环境中定义。
听起来很抽象,但它指出了闭包定义的两个要素:允许自由变量,由定义环境而不是执行环境决定。
先说说自由变量。凡是不是在该代码块中定义的变量就叫做自由变量。如
function g() {return x;}
其中的x并不是在这个函数中定义的,它的值是存储在函数外部的。所以它就是一个自由变量。
那么一个代码块中的自由变量该到哪里去取呢?这个问题即是决定了这个代码块是否属于闭包。闭包定义要求,这个代码块中的自由变量,应该到定义它的环境中取。这也就是说,这个代码块的作用域应该是词法(静态)作用域。
由于同一个代码块,无论被调用多少次,或是在何处调用,它的定义只会在同一个环境中(否则就不能被认为是“同一个代码块”,因为定义环境不同)。闭包要求其自由变量的读取是在定义环境中决定,也就得到它的一个特性:闭包的自由变量永远指向同一处,无论它在何处被调用。
或许这样说,还是有点抽象。我做一个现实中的比喻。我之前为这个blog做模板的时候,需要在本机中装一个PHP+MySQL的环境。这就遇到一个问题:我白天在公司,晚上在家。如果白天做的时候对服务器配置做了一些调整,那么即使把所有的wordpress程序都copy回家,也无法在家接着做。如果把Wordpress程序文件看作是一个函数,它明显是受它的执行环境所影响的。换句话说,Wordpress有一部分“变量”,不是它自身定义的,而是从外部(PHP服务器或MySQL数据库)中得到的。
后来就找到一个很不错的软件,叫Uniform Server。它是一个文件包,里面包含了Apache和MySQL的环境。把它Copy到U盘上之后,点击Start.bat,就会自动在机器上建立一个服务器环境。更关键的是,下班回家时,关掉电脑,拔出移动硬盘带回家,在家里的电脑重新启动它,它就又可以在家里的电脑上重新建立一个服务器环境,而这个环境跟办公室电脑里一模一样,之前在公司作的改动,回到家里后还存在。
所以呢,可以认为这个Uniform Server就是一个闭包。它的变量(服务器配置)是在它被定义(也就是当我解压到U盘上)的时候就确定了的,而跟它在什么地方被调用执行(开启这个软件)无关。
其实从这个比喻中也可以看出,闭包的好处:作用域始终保持一样,不用担心执行环境不同导致的作用域不同。
最后举一个很简单的例子,来帮助理解闭包(我这里用jquery的写法,以简化代码):
var t = $('input#text').val() //取得文本框的值t
$('input#btn').click(function() {
doSth(t); //把t传给某个函数
//... 其他操作
});
这个例子稍微有点怪怪的,因为它的文本框的值是在绑定点击事件之前去取,而不是在点击的时候。我们姑且就把它当作是因为某些特殊的要求不得不这样做吧。所以呢,绑定到点击事件上的那个匿名函数中,t其实使用的是它上一层作用域里的变量t。
这个时候呢,需求有点改变:要求在点击按钮之后,先弹出来一个对话框,和用户进行某些交互(比如要求登录),之后再按原来的步骤继续。为了通用性,我们需要建一个函数:
function openDialog(f) {
// ... 对话框要作的一些操作
f() //再执行后续的操作
}
这里的openDialog函数是打开一个通用的对话框,完成对话框的操作之后呢,再执行后续操作。这里的后续操作可以是一个函数,通过f变量传进openDialog里,让它来决定什么时候调用。
而之前的第一段代码就可以改为:
var t = $('input#text').val() //取得文本框的值t
$('input#btn').click(function() {
function f() {
doSth(t);
//... 其他操作
}
openDialog(f);
});
和第一段代码比较一下,我们只是简单的把原先的代码用一个函数装起来,然后传给openDialog,就搞定啦。
可能有人会觉得,这个本来就是自然而然的事情啊。其实这里面的关键在于,doSth(t);以及其他语句的执行,已经由原来的点击事件的地方,换到openDialog函数里去了,而后者的代码却是写在不知道什么地方的。之所以可以“自然而然”的随便把代码到处“扔”,就是因为闭包的特性(再想想我那个U盘)。
事实上,如果有兴趣,再仔细琢磨一下jquery的那个click函数,会发现,其实它也是因为有闭包,所以才可以把那个匿名函数扔进去(click函数本身的定义和执行,也是在遥远的未知的地方),从而实现绑定点击事件。