JavaScript中闭包的概念和作用

1. 闭包的概念

要想理解什么是闭包,首先要了解变量作用域和作用域链的概念。

1.1 变量作用域

一个变量的作用域(scope)是程序源代码中定义这个变量的区域。在ES6之前,只有全局变量和局部变量,全局变量拥有全局作用域,在JavaScript代码中的任何地方都是有定义。局部变量在函数内声明,作用域是局部性的,只在函数体内有定义,函数参数也是局部变量。

在函数体内,局部变量的优先级高于同名的全局变量。如果在函数内声明的一个局部变量(或者函数参数中带有的变量)与全局变量同名,那么全局变量就被局部变量所覆盖。
看下面一段代码:

var scope="global";//声明一个全局变量
function checkscope(){
var scope="local";//声明一个同名的局部变量
return scope;//返回局部变量的值,而不是全局变量的值
}
checkscope()//返回"local"

checkscope()会返回字符串"local"

1.2 变量作用域链

函数也可以看作是对象。如果将一个局部变量看做是自定义实现的对象属性的话,那么可以换个角度来解读变量作用域。在JavaScript的最顶层代码中(也就是不包含在任何函数定义内的代码),作用域链由一个全局对象组成。在不包含嵌套的函数体内,作用域链上有两个对象,第一个是函数(定义函数参数和局部变量的对象),第二个是全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象。

每一段JavaScript代码(全局代码或函数)都有一个与之关联的作用域链(scope chain)。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量

var scope="global";//位于全局对象(一个对象)
function local(){
    var scope="local";//位于不含嵌套的函数体内(两个对象)
}
function local(){
    function local_local(){
        var scope="local_local";//位于嵌套的函数体内(三个对象)
    }
}

当JavaScript需要查找变量x的值的时候(这个过程称做“变量解析”(variable resolution)),它会遍历作用域链中所有对象的所有属性,如果某个对象有一个名为x的属性,则会直接使用这个属性的值。如果作用域链上没有任何一个对象含有属性x,那么就认为这段代码的作用域链上不存在x,并最终抛出一个引用错误(ReferenceError)异常。

当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。对于嵌套函数来讲,事情变得更加有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都有微妙的差别——在每次调用外部函数时,内部函数的代码都是相同的,而且关联这段代码的作用域链也不相同。

1.3 “闭包”

下面进入正题。

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

当调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链时,事情就变得非常微妙。

JavaScript函数在定义的时候创建了作用域链,之后的调用不会使作用域链发生变化。这句话很重要,揭示了JS函数的一个重要特点:函数在定义它的作用域中执行,而不是在调用它的作用域中执行

请看如下代码:

var scope="global scope";//全局变量
function checkscope(){
var scope="local scope";//局部变量
function f(){return scope;}//在作用域中返回这个值
return f;
}
checkscope()()//返回"local scope"

嵌套的函数f()定义在function checkscope()里,其中的变量scope是局部变量,值为"local scope",不管在何时何地执行函数f(),这种绑定在执行f()时依然有效。因此最后一行代码返回"local scope"。

通过闭包可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义它们的外部函数。下面一段是比较权威的解释。

我们将作用域链描述为一个对象列表,不是绑定的栈。每次调用JavaScript函数的时候,都会为之创建一个新的对象用来保存局部变量,把这个对象添加至作用域链中。当函数返回的时候,就从作用域链中将这个绑定变量的对象删除。如果不存在嵌套的函数,也没有其他引用指向这个绑定对象,它就会被当做垃圾回收掉。如果定义了嵌套的函数,每个嵌套的函数都各自对应一个作用域链,并且这个作用域链指向一个变量绑定对象。但如果这些嵌套的函数对象在外部函数中保存下来,那么它们也会和所指向的变量绑定对象一样当做垃圾回收。但是如果这个函数定义了嵌套的函数,并将它作为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的函数。它就不会被当做垃圾回收,并且它所指向的变量绑定对象也不会被当做垃圾回收。

下面看一个例子:

function counter(){
var n=0;
return{
count:function(){return n++;},
reset:function(){n=0;}
};
}
var c=counter(),d=counter();//创建两个计数器
c.count()//=>0
d.count()//=>0:它们互不干扰
c.reset()//reset()和count()方法共享状态
c.count()//=>0:因为重置了c
d.count()//=>1:而没有重置d

counter()函数返回了一个“计数器”对象,这个对象包含两个方法:count()返回下一个整数,reset()将计数器重置为内部状态。首先要理解,这两个方法都可以访问私有变量n。再者,每次调用counter()都会创建一个新的作用域链和一个新的私有变量。因此,如果调用counter()两次,则会得到两个计数器对象,而且彼此包含不同的私有变量,调用其中一个计数器对象的count()或reset()不会影响到另外一个对象。

2. 闭包的作用

根据第一部分的分析,不难发现,通过闭包这一技术手段,可以在函数外部间接调用函数内部的局部变量,可以让这些变量的值始终保持在内存中(因此要注意不能滥用闭包),还可以给变量开辟私密空间,避免外部污染

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章