JS闭包
闭包是JavaScript语言的一个特点,也是一个难点,许多的高级应用都要依靠闭包来实现,在初学JS时也对闭包的概念感到十分模糊,在面试中这也是经常被问到的问题。
变量的作用域
要想理闭包的概念就首先要明白JS中的变量的作用域。一个变量的作用域就是源代码中定义这个变量的区域。全局变量拥有全局作用域,在JS代码中任何地方都是有定义的。然而在函数内声明的变量只在函数体内有定义。它们是局部变量,作用域也是有局部性的,函数的参数也是局部变量。
作用域链
作用域链的概念对于理解闭包的概念也有着至关重要的作用。
JS是基于词法作用域的语言:通过阅读包含变量定义在内的数行源码就能知道变量的作用域。
如果将一个局部变量看作是自定义实现的对象的属性的话,那么可以换个角度来解读变量作用域。每一段JS代码都有一个与之关联的作用域链。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域”中的变量。当JS需要查找变量X的值的时候(这个过程叫做变量解析),它会从链中的第一个对象开始查找,如果这个对象有一个名为X的属性,则会直接使用这个属性的值,如果第一个对象种不存在名为X的属性,JS会继续查找链上的下一个对象。如果第二个对象依然没有名为X的属性,则会继续查找下一个对象,以此类推。如果作用域链上没有任何一个对象含有属性X,那么就认为这段代码的作用域链上不存在X。
在JS最顶层代码中(也就是不包含在任何函数定义内的代码),作用域链由一个全局对象组成。在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数的局部变量的对象。第二个这时全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象。当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来储存它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。
闭包的概念
在JS中,和其他大多数现代编程语言一样,函数的执行依赖于变量的作用域,这个作用域是函数定义时决定的,而不是函数调用时决定的。为了实现这种词法作用域,JS函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链,函数体内部的变量都可以保存在函数的作用域内,这种特性就被称为“闭包”。
当调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链时,事情就变得有些微妙。当一个函数嵌套了另一个函数,外部函数将嵌套的函数的对象作为返回值返回的时候往往会发生这种事情。当第一次碰到闭包时就觉得十分令人费解。
要想理解闭包首先要了解嵌套函数的词法作用域规则。就比如下面这段代码:
var scope = "global scope"; //全局变量
function checkscope(){
var scope = "local scope"; //局部变量
function f() { return scope; } //在作用域中返回这个值
return f();
}
checkscope() // 输出 "local scope"
checkscope()
函数声明了一个局部变量,并定义了一个函数f(),函数f()反悔了这个变量的值,最后将函数f()的执行结果返回。现在我们非常清楚调用checkscope()
会返回什么,但我们现在要对代码做出一点改动。
var scope = "global scope"; //全局变量
function checkscope(){
var scope = "local scope"; //局部变量
function f() { return scope; } //在作用域中返回这个值
return f;
}
checkscope()()
在这段代码中我们将函数内的一对圆括号移动到了checkscope()
之后。checkscope()
现在返回函数内嵌套的一个函数对象,而不是将结果直接返回,那么这样会发生什么呢?
回想一下词法作用域的基本规则:JS函数的执行到了作用域链,这个域链是函数定义的时候创建的,嵌套函数f()定义在这个作用域链里,其中的变量scope一定是局部变量,不管在何时何地执行函数f(),这种绑定在执行f()时依然有效,因此最后一段代码返回的还是“local scope”。
闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
怎么来理解这句话呢?请看下面的代码。
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
使用闭包的注意点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。