闭包这一词很抽象,事实上概念并不抽象,但是应用起来还真的是很抽象。
先来看看定义(引自百度百科):
闭包是可以包含自由变(未绑定到特定对象)量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在代码块的环境中定义。“闭包”一词来源于以下两者的结合:要执行的代码块和自由变量提供绑定的计算环境。
读完后感觉困惑是很正常的,我现在来好好解释下。
为了方便解释,我们给定义中出现的角色赋上代号,首先我们命名闭包为snippet,包含的自由变量为fvar,全局上下文变量统称为globals,代码块环境是context。
根据定义,我们可以如下改述:
snippet含有fvar,此fvar在snippet中没有定义,同时globals中也不含有fvar的定义,所以,当我们单看snippet时候,你会发现fvar是一个未定义的变量。当我们把snippet放到context中,为了让其正常执行,我们需要补全fvar的定义,但又不能在snippet中定义。所以就要在context中定义。
有了上面改述的内容,我们再看一个实例,来加深理解,下面的代码,我使用JavaScript解释:
var displayItemName= function (item){
var itemDefault = “ default ”;
var closure1 = function (){
alert(item);
}
var closure2 = function (){
alert(itemDefault);
}
closure1();
closure2();
}
displayItemName(“football”)
这里的displayItemName函数对应context,这里没有globals,closure对应我们前面说的snippet,closure中的item和itemDefault,对于closure来说是自由变量,也即对应fvar。但是对于context(displayItemName函数)来说,不是自由变量,并在其中定义了。
从这么多角度说,应该能搞清楚闭包的意思了。接下来我们用一句话总结闭包(针对JavaScript),就是:
在一个函数(父函数)中,定义另一个函数(子函数),子函数用到了父函数的变量,但自己并没有重新定义此变量,此时称该子函数为一个闭包。
上面的定义应该比原本的定义好理解多了,但要注意这主要是根据JavaScript的特性总结的,不同的语言室不同的。
有了闭包的理解后,接下来就是应用,闭包有什么用呢?
闭包主要用于面向函数编程的语言,其以函数为第一性,JavaScript就是这样的语言。我们继续用JavaScript说明。JavaScript本身没有class关键字,如果要实现面向对象该怎么办呢?有很多办法,可以采用module模式,也可以有效利用prototype和数据结构达到目的。这并不是本文重点,在此就不多讨论了,这里我们要说的是利用闭包,来实现面向对象。
首先让我们先看段php代码,
public class People{
private $key = “房门钥匙”;
function openDoor()
{
echo “使用了”. $this ->key.”,开了门”;
}
}
//使用+运行
$people = new People;
$people ->openDoor();
同样再来看看JavaScript版的
function People()
{
var key = “房门钥匙”;
this .openDoor = function (){
alert(“使用了”+key+”,开了门”);
}
}
//使用+运行
var people = new People;
people.openDoor();
对了,就是如此的相似!不过,这里要注意,JavaScript版本中的key不能是this.key。因为我们这里使用的var,只对函数体内部有用,相当于private。如果采用this.key的话,在我们这里会直接指向people对象中的key,而key又是private的,所以this.key相当于people.key是能起作用的。这里有点难理解,你可能需要看我的另一篇博文(面向对象的Javascript和Prototype的理解),了解对象、函数的关系。
这里我们只是体现使用context环境中的变量,通过闭包,我们当然还可以修改其中的值。
接下来,让我们更进一步,了解一些稍微高级点的用法,利用闭包添加或改变函数的属性(效果类似于静态,生命周期与函数一致),阅读以下代码:
var displayItemName= function (item){
var itemDefault = "default" ;
var closure1 = function (){
item=item+ " changed" ;
displayItemName.changed=item; //添加函数属性
displayItemName.addfunction= function (){alert( "addfunc" );} //添加函数方法
}
var closure2 = function (){
alert( "item:" +item+ ";changed property:" +displayItemName.changed);
}
closure1();
closure2();
}
displayItemName( "football" );
alert( "breakpiont" );
这里我们使用chorme进行变量观察,一般情况,如果没有closure1中对函数属性的操作时,我们看到的displayItemName应该是这样的:
但现在的函数的情况是这样的:
看到其中的changed和addfunction属性了吗?这两个都是闭包添加的。
让我们展开constructor和prototype,有下图的情况
我们发现在constructor中没有addfunction和changed属性,而prototype中是有的。我们知道prototype是服务于new出来的object的,所以,我们可以大胆猜测,如果我们运行如下代码:
var testobj=new displayItemName();
testobj.constructor.addfunction();
一定可以正确运行。运行之,的确如此,我们再来看看其对象结构。
这里我们可以认为,原来的displayItemName函数存到了testobj.constructor中了。这个其实和JavaScript的基本数据结构有很大关系。Ok,这里我们看到了,又从另一个角度说明了constructor。
扩展
对于C#这样的语言中,闭包的概念也还是存在的,主要的体现就是委派。我们经常在使用委托的时候,会在委托的函数体中使用前面定义的变量,这其实就构成了一种闭包。只是这种形式看上去不太明显,同时,这里的闭包,由于需要编译成IL,也会产生一些意外情况,主要是时间差引起的,关于这个问题可以参看C#中匿名方法变量共享原理分析,同样的,对于Action,Func,等自然也是,毕竟他们都是委托过来的。这里就不详述了。
整篇文章还是有点长的,接下来让我们做点总结:
1、 说明了闭包的概念
2、 闭包的基本应用
3、 闭包的中级应用
4、 JavaScript中伪静态变量(函数属性)的原理分析
5、 C#中的闭包体现
以上就是本文所讲的主要内容