写在最前:和其他大多数高级编程语言一样,JavaScript也采用词法作用域。
为了实现词法作用域,JavaScript函数对象的内部状态不仅包含函数的逻辑代码,还必须引用当前的作用域链。函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包”。
从技术角度来讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链。定义大多数函数时的作用域链在调用函数时依然有效,但这并不影响闭包。当调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链时,事情就变得非常微妙。当一个函数嵌套了另外一个函数,外部函数将嵌套的函数对象作为返回值返回的时候往往会发生这样的事情。有很多强大的编程技术都利用到了这类嵌套的函数变薄,以至于这种编程模式在JavaScript种非常常见。当你第一次碰到闭包时可能会觉得非常令人费解,一旦你理解掌握了闭包之后,就能非常自如地使用它了,了解这一点至关重要。
理解闭包首先要了解嵌套函数的词法作用域规则。看一下这段代码:
var scope='global scope'; //全局变量
function checkscope(){
var scope='local scope'; //局部变量
function f(){ return scope; } //在作用域中返回这个值
return f();
}
checkscope() //最终结果"local scope"
checkscope()函数声明了一个局部变量,并定义了一个函数f(),函数f()返回了这个变量的值,最后将函数f()的执行结果返回。你应当清楚为什么调用checkscope()会返回"local scope"。现在我们来对这段代码做小小的改动。
var scope='global scope'; //全局变量
function checkscope(){
var scope='local scope'; //局部变量
function f(){ return scope; } //在作用域中返回这个值
return f;
}
checkscope()() //最终结果是多少呢?
在上面的代码中,我们将函数内的一对圆括号移动到了checkscope()之后。checkscope()现在仅仅返回函数内嵌套的一个函数对象,而不是直接返回对象。在定义函数的作用域外面,调用这个嵌套函数会发生什么呢?
JavaScript函数的执行用到了作用域链,这个作用域链是函数定义的时候创建的。嵌套的函数f()定义在这个作用域链里面,其中的scope一定是局部变量,不管在何时何地执行f()函数,结果都是返回"local scope"。闭包的这个特性强大到令人吃惊:它们可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义它们的外部函数。
下面的这个例子,函数每次调用都会产生唯一的一个整数。
var uniqueInterger = (function () { //定义函数并立即执行
var counter = 0; //函数的私有状态
return function () { return counter++; };
}());
你需要仔细阅读这段代码才能理解其含义。粗略来看像是函数赋值给了一个变量uniqueInterger,实际上,这段代码定义了一个立即调用的函数,因此是这个函数的返回值赋值给了变量uniqueInterger。现在,我们来看下函数体,这个函数返回了另外一个函数,这是一个嵌套函数,我们将它赋值给变量uniqueInterger(现在的uniqueInterger等同function(){return counter++},而且可以访问到counter),嵌套的函数时可以访问作用域内的变量的,而且可以访问外部函数中定义的counter变量。当外部函数返回之后,其他任何代码都无法访问counter变量,只有内部的变量才能访问它。
像counter一样的私有变量不是只能在一个单独的闭包内,在同一个外部函数内定义的多个嵌套函数也可以访问它,这多个嵌套函数都共享一个作用域链,看一下这段代码:
function counter() {
var n = 0;
return {
count: function () {
return n++;
},
reset: function () {
return n = 0;
}
};
}
var c = counter(),d=counter();
c.count() //返回0
d.count() //返回0 互不干扰
c.reset() //reset()和count()方法共享状态
c.count() //返回0 因为我们上面重置了
d.count() //返回1 因为我们没有重置d
counter()函数返回了一个“计数器”对象,这个对象包含了两个方法:count()返回下一个整数,reset()将计数器重置为内部状态。首要要理解,这个两个方法都可以访问私有变量n。再者,每次调用counter()都会创建一个新的作用域链和一个新的私有变量。因为,如果调用counter()两次,则会得到两个计数器对象,而且彼此包含不同的使用变量,调用其中一个计数器对象的count()或reset()不会影响到另外一个对象。
我们已经给出了一些例子,在同一个作用域链中定义两个闭包,这两个闭包共享同样的私用变量或变量。这是一个非常重要的技术,但还是要特别小心那些不希望共享的变量往往不经意间共享给了其他的闭包,了解这一点也很重要,我们看下面一段代码:
//这个函数返回一个总是返回v的函数
function constfunc(v) {
return function () {
return v;
};
}
//创建一个数组用来储存常数函数
var funcs = [];
for( let i = 0; i < 10; i++) funcs[i] = constfunc(i);
//在第5个位置的元素所在的函数返回值为5
funcs[i]() //结果为5
这段代码利用循环创建了很多个闭包,当写类似这种代码的时候往往会犯一个错误:那就是试图将循环代码移入定义这个闭包的函数之内,看下这段代码:
//返回一个函数组成的数组,它们的返回值是0-9
function constfunc() {
var funcs = [];
for( let i=0; i<10; i++){
funcs[i] = function () {
return i;
}
}
return funcs;
}
var funcs = constfunc();
funcs[5]() //会返回什么?
上面这段代码创建了10个闭包,并将它们保存到了一个数组中。这些闭包都是在同一个函数调用中定义的,因此他们共享同一个变量i。当counstfuncs()返回时,变量i的值是10,所以所有的闭包都共享这个值,因此,数组中的函数的返回值都是同一个值,这不是我们想要的结果。关联到闭包的作用域链都是“活动的”,记住这一点非常重要。嵌套的函数不会将作用域内的使用变量复制一份,也不会对所有绑定的变量生产静态快照。
网友评论