在JavaScript里,“闭包”是一个神奇的东西。借着闭包的力量,我们将跨过面向对象的领域,来攀登一座新的高峰。保罗格雷厄姆 曾经说过, 我 认为目前为止只有两种真正干净利落, 始终如一的编程模式:C语言模式和Lisp语言模式.此二者就象两座高地, 在它们中间是尤如沼泽的低地。在这里C语言代表着过程式语言的精髓,它目前所知的高层境界是面向对象。而称为Lisp的语言,则以另一种形式的无与伦比的 美,成为与过程化对等的存在,即,我们将要介绍的函数式编程。
22.1动态语言与闭包
程序语言中的闭包(closure)概念不是由JavaScript最先提出的,从smalltalk开始,闭包就成了编程语言的一个重要概念。几乎所有的知名动态语言(如Perl、python、ruby等)都支持闭包,JavaScript也不例外。
闭包 ( closure ) 的确是个精确但又很难解释的电脑名词。因此在理解它之前,必须先解释下面一些简单概念。
22.1.1动态语言
所谓动态程序设计语言(Dynamic Programming Language),准确地说,是指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等在结构上的变化。相反,非动态语言在编译(或解释)时,程序结构已经被确定,在执行过程中不能再发生改变。
JavaScript是一个典型的动态语言。除此之外如Ruby、Python等也都属于动态语言,而C、C++等语言则不属于动态语言。
一些人习惯上 将编译型语言认为是非动态语言,而解释型语言认为是动态语言,实际上这是完全错误的概念,动态语言的概念与语言是编译还是解释没有关系,一些解释型的语言 也可以是静态语言,编译型语言确实不易设计为动态语言,但也仍然可以通过良好的设计和使用技巧达到“动态”的效果。
在这里还需要区分一下另外一对容易和上面概念混淆的概念,即动态类型语言(Dynamically Typed Language) 和静态类型语言(Static Typed Language)。
所谓动态类型语言是指在执行期间才去发现数据类型的语言,静态类型语言与之相反,如JavaScript、VBScript和Perl都是典型的动态类型 语言。很多人常常将动态类型语言和动态语言混为一谈,显然从上面的描述看来,它们是两个完全不同的概念。虽然,大多数动态语言都是动态类型语言,但动态语 言本身并不要求一定是动态类型的,而动态类型语言也不一定是动态语言。
22.1.2语法域和执行域
所谓语法域,是指定义某个程序段落时的区域,所谓执行域则是指实际调用某个程序段落时所影响的区域。
在非动态语言中,语法域和执行域范围基本上是一致的,执行域通常只能访问它自身语法域的范围和少量向它开放的语法域,而不能访问它外层或者与它关联的执行域。而在动态语言中,执行域的范围通常大得多。
非动态语言, 如C++的函数在调用时(执行域上)只能访问自身语法域上允许访问的环境,如全局变量和函数、所在对象的属性和方法以及自身的参数和临时变量,这和定义函 数时的许可范围一致。动态语言如JavaScript的函数不但能够访问语法域上的这些范围,还能够访问它外层环境中的执行域范围,例如:
例22.1 动态语言的执行域
<html>
<head>
<title>Example-22.1 动态语言的执行域</title>
</head>
<body>
<script>
<!—
//产生随机数的函数
function RandomAlert()
{
var x = Math.random()
return function()
{
alert(x);
}
}
var a = RandomAlert();
//闭包的执行域随函数调用而创建
var b = RandomAlert();
a(); //调用a,打印出产生的随机数
b(); //调用b,打印出产生的随机数
//一般情况下,a和b得到的数值不同
-->
</script>
</body>
</html>
22.1.2 JavaScript的闭包
在程序语言中,所谓闭包,是指语法域位于某个特定的区域,具有持续参照(读写)位于该区域内自身范围之外的执行域上的非持久型变量值能力的段落。这些外部执行域的非持久型变量神奇地保留它们在闭包最初定义(或创建)时的值 (深连结)。
从上面的概念可以看出,闭包通常是在动态语言中才有的概念,它是某些可以访问外部执行域的段落。JavaScript中的闭包,是通过定义在函数体内部的function来实现的。
例22.1就是典型的闭包应用,RandomAlert()函数的返回值是一个闭包,a(),b()分别访问了闭包两次被创建时对应的外层RandomAlert()函数的执行域上的局部变量x的值。
闭包这个概念我们之前已经多次提到过,但是一直没有解释清楚。相信你即使看了本节前面两段的 解释,仍然还是会觉得有一点困惑。闭包和函数的概念到底有什么相同点和不同点,相信这是大多数读到这里的读者心中最大的疑惑。其实,闭包和函数的关系,应 当类似于一种动态和静态、结构和实例的关系,下面再通过一个例子来简单说明:
例22.2 闭包的本质
<html>
<head>
<title>Example-22.2 闭包的本质</title>
</head>
<body>
<script>
<!—
//A是一个普通的函数
function A(a)
{
return a;
}
//B是一个带函数返回值的函数
function B(b)
{
return function (){
return b;
}
}
var x = A(10);
//因为A除了返回a外什么也没做,执行A()函数后,调用堆栈被销毁
//没有产生闭包,或者说在调用“瞬间”产生了闭包,然后马上被销毁
var y = B(20);
//因为B返回了一个匿名函数引用,它访问到B()被调用时产生的环境
//因此这里产生了一个“闭包结构”(closure或者function instance)
//在它的环境中,b = 20,因此y()的返回结果是20
var z = B(30);
//同样,这里产生了第二个“闭包结构”
//在它的环境中,b = 30,因此z()的返回结果是30
alert(x); //得到10
alert(y()); //得到20
alert(z()); //得到30
-->
</script>
</body>
</html>
我们说例22.2中,y()和z()的结果不同,因为两次B()创造的闭包被执行时访问的是不同的b值,它正好是分别的调用B()时b被初始化的值。这里 最奇怪的地方在于,当y()和z()被调用时,B()函数调用已经结束了。如果你有C++、Java或者其他什么编程语言的知识,也许你的潜意识里会认为 当B()调用结束时,局部变量b的值已经被销毁,但结果却是令人惊讶的,由于被返回的闭包里引用了B()调用域上的b值,所以它并没有随着B()调用的结 束而被销毁。
类似的还有之前我们见到过的例22.1和例4.6,在这里我们再次列出例4.6:
function dice(count, side) //count定义骰子的数量,side定义每个骰子的面数
{
var ench = Math.floor(Math.random() * 6); //+0~+5的骰子随机变数修正
//这里返回一个闭包,该闭包的作用是对指定的面数和修正值的骰子进行“投掷”
return function()
{
var score = 0;
for(var i = 0; i < count; i++)
{
score += Math.floor(Math.random() * side) +1;
}
return score + ench;
}
}
var d1 = dice(2,6); //生成一组2d6+n的骰子,其中的n为0~5的随机数
var d2 = dice(1,20); //生成一颗20面的骰子,带有0~5的随机点数修正
例4.6中,d1、d2引用的闭包都使用了外部环境 中的局部变量ench和side的值,而这两个局部变量是在dice()方法才被初始化的,在dice()调用结束后,它们并没有被销毁。当你调用 d1()和d2()时,你将会引用到d1和d2在获取闭包时分别创建的side和ench值。
我通常认为闭包是一种引用结构,至少在JavaScript中是这样的。JavaScript中的闭包(closure),也可以理解为一种“函数实例引用”(function instatnce referer)。