关于界面的价值观与方法论

作用域链 词法作用域 与闭包(三)

15February2009

在经历了漫长的作用域链、调用对象和词法作用域之后,终于可以讨论闭包了。

对于闭包这个概念,有各种各样的解释。我比较喜欢的是闭包是《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函数本身的定义和执行,也是在遥远的未知的地方),从而实现绑定点击事件。


  1. mr:

    经常误解闭包的我学习了。

  2. pbqi:

    还是那句话,很感谢