也谈闭包--小白的JS进阶之路

JavaScript当然是会用的,不过没有深入系统的学习罢了。平常还是用JQuery比较多,原生的JS用到的很少。
不过前端时间学习Ruby,被动态语言的强大和魔幻给震惊了一把。了解Ruby后,我把目光转移到了这门很早就伴随我的语言。JQuery是一个华丽的外衣,一把好用的工具,但炫彩背后必有强大的后台,让我稍微掀起这幕布的一角吧。

作用域链

闭包这个词想必大家不陌生,可谓是动态语言了不起的几把刷子之一了。在JS中,要想理解闭包,首先要拿作用域链这个概念下手。
先来看一下JS语言的另一种定义:JS是基于词法作用域的语言--通过阅读包含变量定义在内的源码就能知道变量的作用域。全局变量在程序中始终是有定义的,局部变量在声明它的函数体内以及所嵌套的函数内始终是有定义的。
在顶层代码中,作用域链由一个全局对象组成。在不包含嵌套的函数体内,作用域链由两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象。
当定义一个函数时,它实际上保存一个作用域链(很重要,后面靠它混饭了,注意是定义的时候哦)。当调用函数时,它创建一个新的对象来储存它的局部变量,并将这个对象添加至保存的那个作用域链上,同时插进一个新的更长的表示函数调用作用域的链。
对于嵌套函数来说,每次调用外部函数时,内部函数又会重新定义一遍。

这段定义看完之后,估计要彻底晕了。没关系,先对作用域链有个印象即可,后面才是大菜

闭包定义

函数对象可以通过作用域链相互关联,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中叫“闭包”。

JS闭包

JS采用词法作用域,也就是说,函数的执行依赖于变量作用域,这个作用域是函数定义时决定的,而不是函数调用时决定的
为了实现语法作用域,JS函数对象内部状态引用当前的作用域链。
从技术角度看,所有JS函数都是闭包:它们都是对象,它们都关联到作用域链。
当调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链时,事情就变得非常微妙。当一个函数嵌套了另外一个函数,外部函数将嵌套的函数对象作为返回值返回的时候往往会发生这种事情。很多强大的编程技术都利用了这类嵌套的函数闭包,以至于这种编程模式在JS中非常常见。
呼呼,这段话看完估计同志们又有点丈二和尚了,学术语言害死人啊。不过还是那句话:没关系,继续看。JS没有密不透风的墙啊。

嵌套函数的语法作用域

理解闭包首先要理解嵌套函数的语法作用域规则,看一下这段代码:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f() { reutrn scope; };
    return f();
}
checkscopre()             //"local scope"

因为返回的是f()的执行结果,所以对结果我们没有疑问。
稍微改动一下这个函数:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f() { reutrn scope; };
    return f;
}
checkscopre()()             //?

考虑一下哦,这个结果如何呢?checkscopre()返回一个函数,然后我们再执行这个返回的函数,那么很多人认为返回结果就是"global scope"喽。
饿,这其实就是作用域链的陷阱了。回想词法作用域的基本规则:JS函数的执行用到作用域链,这个作用域链是函数定义时创建的。 所以不管何时执行函数f(),这种绑定在执行f()时依然有效。创建时定义的作用域链scope的值是"local scope",那么因此返回结果也是”local scope”
简单来说,闭包这个特性让人吃惊:它们可以捕捉到局部变量(和参数),并一直保持下来,看起来像这些变量绑定到了在其中定义它们的外部函数(有点绕,建议阅读3遍)

解惑词法作用域规则

如果理解了词法作用域的规则,就很容易理解闭包:定义大多数函数时的作用域链在调用函数时依然有效。
理解闭包的困难在于:外部函数中定义的局部变量在函数返回后就不存在了,那么嵌套的函数如何能调用不存在的作用域链呢?
想搞清楚这个问题,就需要深入了解类似C语言这种更底层的编程语言,并了解基于栈的CPU架构:如果一个函数的局部变量定义在CPU栈中,那么函数返回时它们确实不存在了。
但回想一下我们是如何定义作用域链的。我们将作用域链描述为一个对象列表,而不是一个栈。每次调用JS函数,都会创建新的对象保持局部变量,把这个对象添加至作用域链中。当函数返回,就删除作用域链中的绑定变量的对象。如果不存在嵌套函数,也没有其他引用指向这个绑定变量的对象,它就会被垃圾回收。如果定义了嵌套函数,每个嵌套函数各自对应一个作用域链,并且这个作用域链指向一个变量绑定对象。
如果这个嵌套函数在外部函数中保存下来,那么它们会和指向变量绑定对象一样被垃圾回收。
但如果外部函数将嵌套函数作为返回值返回或储存在某个属性里,这时就有一个外部引用指向这个嵌套函数。它就不会被当做垃圾回收,所以它指向的变量绑定对象也不会被当做垃圾回收。
看到这里相比对闭包有一定了解了,拨云见月时机不远矣。下面通过几个例子练练手,想必闭包从此就是囊中之物喽。

闭包实现计数器

先看一个不健全的计数器实现:

uniqueInteger.counter = 0;
function uniqueInteger() {
    return uniqueInteger.counter++;
}

这里函数作为对象被赋予了属性counter ,调用uniqueInteger(),就是一个简单的计数器。
但明显的缺陷是:如果恶意的把counter重置或赋值给它一个非整数,就会导致函数错误。
利用闭包很好的解决这个问题,只要把计数器私有就可以了:

var uniqueInteger = (function() {
                                    var counter = 0;
                                    return function() { return counter++; };
                                }());

uniqueInter就是一个嵌套函数,而counter则变成了私有的局部变量。因为第一次已经执行过了外部函数,所以当调用uniqueInteger()的时候,实际调用的是嵌套函数,这样counter就访问不到,但我们同时实现了功能。
counter一样的私有变量不是只能用在一个单独的闭包内,在同一个外部函数内定义的多个嵌套函数也可以访问它,这多个嵌套函数都共享一个作用域链:

function counter() {
    var n = 0;
    reutrn {
        count : function() { return n++; };
        reset : function() { n = 0; };
    };
}
var c = counter() , d = counter();
c.count()    //0
d.count()    //0,互不干扰
c.reset()     //0
c.count()    //0
d.count()   //1,因为没有重置d

闭包的不当用法

闭包如此美丽,以至于人们如此爱你。但别忘了玫瑰都带刺啊,下面看看闭包使用不当带来的意料之外的后果。
我们先来看一下正确的用法:

function constfunc(v) { return function() { return v; }; }

var funcs = [];
for(var i = 0; i < 10; i++) funcs[i] = constfunc(i);

funcs[5]()    //5

我们稍微变一下形式,看看写类似代码往往会犯的一个错误:

function constfuncs() {
    var funcs = [];
    for(var i = 0; i < 10; i++)
        funcs[i] = function() { return i; };
    return funcs;
}
var funcs = constfuncs();
funcs[5]();    //?

上面的闭包都是在同一个函数调用中定义的,因此他们可以共享变量。当constfuncs()返回时,i=10,所有闭包都共享这个值。因此,数组中函数返回值都是同一个值,这并不是我们想要的。
如果不理解,回想一下词法作用域的定义哦。

后记

至此,闭包就讲完了。可能还是感觉有不理解的地方,如果是这样,那么建议把不理解的地方多读几遍就会明白了。

我是一个小白,但是我也想进步哈!






posted @ 2014-02-20 10:28  轩辕李  阅读(295)  评论(0编辑  收藏  举报