C#、Javascript闭包(closure)

来源:互联网 发布:ise软件win8 编辑:程序博客网 时间:2024/05/18 15:22
          闭包其实就是使用的变量已经脱离其作用域,却由于和作用域存在上下文关系,从而可以在当前环境中继续使用其上文环境中所定义的一种函数对象。下面分别从C#、JavaScript两种语言的角度分别描述下闭包的相关概念,援引两篇小文如下:
 
§ C#

引用:http://kb.cnblogs.com/page/111231/

首先来个最简单的JavaScript中常常见到的关于闭包的例子:

        function f1(){              var n = 999;              return function(){                alert(n); // 999                       return n;              }        }        var a = f1();        alert(a())

这段代码翻译成C#代码就是这样:

    public class TCloser    {        public Func<int> T1()        {            var n = 999;            return () => { 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中的局部变量的生命周转期自动提升,并与匿名函数的生命周期相同,这样就称之为闭合。

  也许你会说,这个返回的委托包含的变量n只是编译器通过某种方式隐藏的对这个委托对象的一个同样对象的赋值吧,那么我们再对比下面两个方法:

        public Func<int> T1()        {            var n = 999;            Func<int> result = () => { return n; };            n = 10;            return result;        }        public dynamic T2()        {            var n = 999;            dynamic result = new { A = n };            n = 10;            return result;        }        static void Main()        {            var a = new TCloser();            var b = a.T1(); var c = a.T2();            Console.WriteLine(b());            Console.WriteLine(c.A);        }

最后输出结果是什么呢?答案是10和999,因为闭包的特性,这里匿名函数中所使用的变量就是实际T1中的变量,与之相反的是,匿名对象result里面的A只是初始化时被赋予了变量n的值,它并不是n,所以后面n改变之后A并未随之而改变。这正是闭包的魔力所在。

  你可能会好奇.NET本身并不支持函数对象,那么这样的特性又是从何而来呢?答案是编译器,我们一看IL代码便会明白了。

  首先我给出C#代码:

public class TCloser    {        public class TCloser        {            public Func<int> T1()            {                var n = 10;                return () => { return n; };            }            public Func<int> T4()            {                return () =>                {                    var n = 10;                    return n;                };            }        }

这两个返回的匿名函数的唯一区别就是返回的委托中变量n的作用域不一样而已,T1中变量n是属于T1的,而在T4中,n则是属于匿名函数本身的

在C#中,原来闭包只是编译器玩的花招而已,它仍然没有脱离.NET对象生命周期的规则。它将需要修改作用域的变量直接封装到返回的类中,变成类的一个属性n,从而保证了变量的生命周期不会随函数T1调用结束而结束,因为变量n在这里已经成了返回的类的一个属性了。

  看到这里我想大家应该大体上了解C#闭包的来龙去脉了吧。C#中,闭包其实和类中其他属性、方法是一样的,它们的原则都是下一层可以畅快的调用上一层定义的各种设定,但上一层则不具备访问下一层设定的能力。即类中方法里的变量可以自由访问类中的所有属性和方法,而闭包又可以访问它的上一层即方法中的各种设定。但类不可以访问方法的局部变量,同理,方法也不可以访问其内部定义的匿名函数所定义的局部变量。

  这正是C#中的闭包,它通过超越Java语言的委托打下了闭包的第一步基础,随后又通过各种语法糖和编译器来实现如今在.NET世界全面开花的Lamda和LINQ,也使得我们能够编写出更加简洁优雅的代码。

  附:后面是吐槽,与上文无关,大家可以略过,这篇文章其实两年之前在给同事讲C#闭包的时候就有想法整理出来和大家分享了,不过因为生活,工作,或许主要还是自己太懒的原因而拖着没动笔,到今天早上看到园友抱怨国内教书育人的氛围才最终决定利用晚上时间把它整理,然后放出来。我个人认为国内技术圈子的氛围尚可,虽然仍然有很多浮躁和易怒在圈子里徘徊,但我们想想国内IT人的生存空间就容易理解了。每天最理想的情况朝9晚6的干活,晚上加班,周末加班这些都是常事,而对我们而言,只要想写出一些经过细细思考的东西都至少需要2个小时以上,而且最好中间不要有人来打扰,这也就注定我们在白天工作时候很难完全有时间静下来组织语言,刨掉这些时间,留给我们自己的生活时间又有多少呢?所以我每次看到有园友发表帖子的时间是晚上1点,2点甚至更晚,都毫不意外,

  我们并非专业写手,也不像国外IT人那样有充足的闲暇时光可以钻研自己的最爱,我们赚着他们的零头,买着比他们本子价格更贵的笔记本,担着比他们更高房价的压力来生活,这样的生活条件下我们这些可爱的社区(不仅限于cnblogs, javaeye, phpchina等)Geek们仍然如此活跃和热情,你还能抱怨什么呢?你要知道你看到的每篇文章(如果是工作人士的话)都是他们晚上从9点写到12点的生活点滴啊。

  所以,以后不要抱怨国内IT氛围吧,相对这个社会其他各行各业的浮躁,我觉得我们的IT圈子已经是很乐于分享的一个群体了。而且除了因为“天下武功,源自欧美,滞后于英语国家”的缘故,我们有些技术确实要晚些才能跟上国外社区的脚步,但对于一些基础知识的解释,已经有很多中文的文章解释得很不错了。像我以前在理解闭包的时候, javaeye上看到的一大堆,像WIKI,像阮一峰的文章,我个人认为对中文用户是足够了。当然,这只是我个人的观点,大家不必较劲。

  最后一点抱怨就是国内大大小小的抄袭网站,我想这也是影响我们中文用户查询资料的一个重要因素吧。以前曾经尝试过在baidu, google上搜索自己的文章,但结果相当令人失望,那些抄袭的网站从来都不在乎内容,因为这些可以通过抄来解决,而且不必带原文链接,不必表明作者。好像东西就是他们自己的一样,他们唯一在乎的就是SEO。这也导致我在使用google搜索的时候时常看到同一篇文章出现在某一页的所有搜索结果中,当然,网址是千奇百怪,实在让人无奈。有些网站即使标明了出处和作者,但用心略有险恶的不是给的链接,而是文字。而且,在这些抄袭者中,最让我感到悲哀的是大名鼎鼎的败毒文库,我至少看到不下5篇败毒文库里的文章是来自JE或者CSDN的,但在文库里面只有个文档,你看不到任何作者提示或者原文链接。也许有人会说,这也可能是作者自己上传的呀,但我个人认为这种可能性太小了,以国内IT人的风格,对败毒即使谈不上厌恶,也很少有主动去巴巴的。试想,一个国内最大的互联网公司都不尊重IT人的劳动(但愿我是错的吧),你又能对其他人说什么呢? 同样看看国外,就我看到的DZone, WindowsPhoneGeek等,每个都是很明确的给出原文的链接,基本上我很少看到有引用别人文章不给原文链接的文章的。而正是这些不尊重作者劳动的网站对国内互联网资料搜索造成大量的垃圾信息。

  引用:

  维基百科: http://zh.wikipedia.org/wiki/%E9%97%AD%E5%8C%85_%28%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6%29
  博客园: http://www.cnblogs.com/frankfang/archive/2011/08/03/2125663.html

 

§ JavaScript
 
引用:http://www.knowsky.com/442121.html
 
         最近在网上查阅了不少Javascript闭包(closure)相关的资料,写的大多是非常的学术和专业。对于初学者来说别说理解闭包了,就连文字叙述都很难看懂。撰写此文的目的就是用最通俗的文字揭开Javascript闭包的真实面目。
一、什么是闭包?
“官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
相信很少有人能直接看懂这句话,因为他描述的太学术。我想用如何在Javascript中创建一个闭包来告诉你什么是闭包,因为跳过闭包的创建过程直接理解闭包的定义是非常困难的。看下面这段代码: 
function a(){
 var i=0;
 function b(){
 alert(++i);
 }
 return b;
}
var c = a();
c();
这段代码有两个特点:
1、函数b嵌套在函数a内部;
2、函数a返回函数b。
这样在执行完var c=a()后,变量c实际上是指向了函数b,再执行c()后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个闭包,为什么?因为函数a外的变量c引用了函数a内的函数b,就是说:

当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个闭包。

我猜想你一定还是不理解闭包,因为你不知道闭包有什么作用,下面让我们继续探索。

二、闭包有什么作用?
简而言之,闭包的作用就是在a执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a所占用的资源,因为a的内部函数b的执行需要依赖a中的变量。这是对闭包作用的非常直白的描述,不专业也不严谨,但大概意思就是这样,理解闭包需要循序渐进的过程。
在上面的例子中,由于闭包的存在使得函数a返回后,a中的i始终存在,这样每次执行c(),i都是自加1后alert出i的值。

那 么我们来想象另一种情况,如果a返回的不是函数b,情况就完全不同了。因为a执行完后,b没有被返回给a的外界,只是被a所引用,而此时a也只会被b引 用,因此函数a和b互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。(关于Javascript的垃圾回收机制将在后面详细介绍)

三、闭包内的微观世界
如 果要更加深入的了解闭包以及函数a和嵌套函数b的关系,我们需要引入另外几个概念:函数的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数a从定义到执行的过程为例阐述这几个概念。

1、当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时a所在的“环境”,如果a是一个全局函数,则scope chain中只有window对象。
2、当函数a执行的时候,a会进入相应的执行环境(excution context)。
3、在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就为第1步中的scope chain。即a.scope=a的作用域链。
4、然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到a的作用域链的最顶端。此时a的作用域链包含了两个对象:a的活动对象和window对象。
5、下一步是在活动对象上添加一个arguments属性,它保存着调用函数a时所传递的参数。
6、最后把所有函数a的形参和内部的函数b的引用也添加到a的活动对象上。在这一步中,完成了函数b的的定义,因此如同第3步,函数b的作用域链被设置为b所被定义的环境,即a的作用域。

到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。

当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象,如下图所示:

如图所示,当在函数b中访问一个变量的时候,搜索顺序是先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索函数a的活动对象,依 次查找,直到找到为止。如果整个作用域链上都无法找到,则返回undefined。如果函数b存在prototype原型对象,则在查找完自身的活动对象 后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。

四、闭包的应用场景
1、保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。
2、在内存中维持一个变量。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。
以上两点是闭包最基本的应用场景,很多经典案例都源于此。

五、Javascript的垃圾回收机制
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。