Javascript的闭包
下面代码中,generateClosure() 函数中有一个局部变量count,初值为0。还有一个叫做 get 的函数,get 将其父作用域,也就是 generateClosure() 函数中的 count 变量增加1,并返回 count 的值。generateClosure() 的返回值是 get 函数。在外部我们通过 counter 变量调用了 generateClosure() 函数并获取了它的返回值,也就是 get 函数,接下来反复调用几次counter(),我们发现每次返回的值都递增了1。1
2
3
4
5
6
7
8
9
10
11
12
13
var generateClosure = function() {
var count = 0;
var get = function() {
count ++;
return count;
};
return get;
};
var counter = generateClosure();
console.log(counter()); // 输出1
console.log(counter()); // 输出2
console.log(counter()); // 输出3
让我们看看上面的例子有什么特点,按照通常命令式编程思维的理解,count 是generateClosure 函数内部的变量,它的生命周期就是generateClosure 被调用的时期,当 generateClosure 从调用栈中返回时,count 变量申请的空间也就被释放。问题是,在 generateClosure() 调用结束后,counter() 却引用了“已经释放了的” count 变量,而且非但没有出错,反而每次调用 counter() 时还修改并返回了count。这是怎么回事呢?
这正是所谓闭包的特性。当一个函数返回它内部定义的一个函数时,就产生了一个闭包,闭包不但包括被返回的函数,还包括这个函数的定义环境。上面例子中,当函数generateClosure() 的内部函数get 被一个外部变量counter 引用时,counter 和generateClosure() 的局部变量就是一个闭包。
C#中也存在闭包
在C#里面,变量作用域有三种,一种是属于类的,我们常称之为field;第二种则属于函数的,我们通常称之为局部变量;还有一种,其实也是属于函数的,不过它的作用范围更小,它只属于函数局部的代码片段,这种同样称之为局部变量。这三种变量的生命周期基本都可以用一句话来说明,每个变量都属于它所寄存的对象,即变量随着其寄存对象生而生和消亡。对应三种作用域我们可以这样说,类里面的变量是随着类的实例化而生,同时伴随着类对象的资源回收而消亡(当然这里不包括非实例化的static和const对象)。而函数(或代码片段)的变量也随着函数(或代码片段)调用开始而生,伴随函数(或代码片段)调用结束而自动由GC释放,它内部变量生命周期满足先进后出的特性。
我们知道,C#, Java和JavaScript, Ruby, Python这些语言不同,在C#和Java的世界里面,原子对象就是类(当然还有struct和基本变量),而不是很多动态语言中的函数,我们可以实例化一个类,实例化一个变量,但不可以直接new 一个函数。也就是表面上看,我们是没办法像js那样将函数进行实例化和传递的。这也是为什么直到Java 7闭包才被姗姗来迟的加入Java特性中。但对C#来说这些只是表象,我刚学C#的时候,看到最多的解释委托的话就是:委托啊,就相当于C++里面的函数指针啦。这句话虽然笼统,但确实有一定道理,通过委托特别是匿名委托这层对象的包装,我们就可以突破无法将函数当做对象传递的限制了。
好像这里还是没讲到闭包和委托的关系,好吧,我太啰嗦了,下面从概念开始讲。
闭包其实就是使用的变量已经脱离其作用域,却由于和作用域存在上下文关系,从而可以在当前环境中继续使用其上文环境中所定义的一种函数对象。
下面小猪把上面的js代码翻译成对应的C#代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TCloser
{
public Func<int> T1()
{
var n =0;
return () =>;
{
n++
Console.WriteLine(n);
return n;
};
}
}
class Program
{
static void Main()
{
var a = new TCloser();
var b = a.T1();
Console.WriteLine(b());
}
}
从上面的代码我们不难看到,变量n实际上是属于函数T1的局部变量,它本来生命周期应该是伴随着函数T1的调用结束而被释放掉的,但这里我们却在返回的委托b中仍然能调用它,这里正是闭包所展示出来的威力。因为T1调用返回的匿名委托的代码片段中我们用到了n,而在编译器看来,这些都是合法的,因为返回的委托b和函数T1存在上下文关系,也就是说匿名委托b是允许使用它所在的函数或者类里面的局部变量的,于是编译器通过一系列动作(具体动作我们后面再说)使b中调用的函数T1的局部变量自动闭合,从而使该局部变量满足新的作用范围。
因此,如果你看到.NET中的闭包,你就可以像js中那样理解它,由于返回的匿名函数对象是在函数T1中生成的,因此相当于它是属于T1的一个属性。如果你把T1的对象级别往上提升一个层次就很好理解了,这里就相当于T1是一个类,而返回的匿名对象则是T1的一个属性,对属性而言,它可以调用它所寄存的对象T1的任何其他属性或者方法,包括T1寄存的对象TCloser内部的其他属性。如果这个匿名函数会被返回给其他对象调用,那么编译器会自动将匿名函数所用到的方法T1中的局部变量的生命周转期自动提升,并与匿名函数的生命周期相同,这样就称之为闭合。
参考:http://kb.cnblogs.com/page/111231/
《Node.js》开发指南