引出闭包
开始之前,先来看看下面的一个经典例子,试着自己想出结果:
let sample = "global scope"
function checkscope(){
let sample = "local scope"
function f() {return sample}
return f()
}
checkscope()
聪明的你一定想到了
checkscope()
函数声明了一个局部变量,然后定义了一个返回该变量的值的函数并调用了该函数。很显然,调用checkscope()
应该返回“local scope”
我们知道:JavaScript
函数可以嵌套定义在其他函数里,内嵌的函数可以访问定义在函数作用域的任何变量。事实上,与多数现代编程语言一样,JavaScript
使用词法作用域(lexical scoping)。
这意味着函数执行时使用的是定义函数时生效的变量作用域,而不是调用函数时生效的变量作用域。
为了实现词法作用域,JavaScript
函数对象的内部状态不仅要包括函数代码,还要包括对函数定义所在作用域的引用。
这种函数对象与作用域(即一组变量绑定)组合起来解析函数变量的机制,在计算机科学文献中被称作闭包(closure)
浅浅了解闭包
有细心的小伙伴看这上面这句话,发现了端倪:
“多数函数调用与函数定义都在同一作用域内,所以闭包有什么用?”
闭包真正值得关注的时候,是定义函数与调用函数的作用域不同的时候。最常见的情形就是一个函数返回了在它内部定义的嵌套函数。很多强大的编程技术都是建立在这种嵌套函数闭包之上的,因此嵌套函数闭包在JavaScript
程序中也变得比较常见。乍一接触闭包难免不好理解,但只有真正理解了,才能用好它们。
还记得最开始的那个经典案例吗,其实他还有“二段式”:
let sample = "global scope"
function checkscope(){
let sample = "local scope"
function f() {return sample}
return f
}
checkscope()() // => f()
我猜有人会说:“这题我会,必是global scrope
”
其实不然:此时刚好函数调用与函数定义不在同一作用域内。而JavaScript
函数是使用定义它们的作用域来执行的。在定义嵌套函数f()
的作用域中,变量sample
绑定的值是“local scope”,该绑定在 f
执行时仍然有效,无论它在哪里执行。
因此前面代码示例最后一行返回“localscope”,而非“global scope”。简言之,这正是闭包惊人且强大的本质:
它们会捕获自身定义所在外部函数的局部变量(及参数)绑定
再看看下面另外一个例子:
let hsiao =(function(){
let counter = 0;
return function(){ return counter++}
}())
hsiao() // 0
hsiao() // 1
作为小白的我当时看到也有点懵,不过不怕,我们慢慢捋
首先我们要知道:第一行代码乍一看像一条赋值语句,把一个函数赋值给变量hsiao
。实际上(如第一行代码开头的左圆括号所提示的),这行代码定义并调用了一个函数,就是立即执行函数。
(function(){
...
})();//更清晰
(function(){
...
}());//w3c建议
因此真正赋给hsiao
的是这个函数的返回值。再仔细看一下函数体,你会发现它的返回值是另一个函数function(){return counter++}
。
换句话说,这个嵌套的函数最终被赋值给了hsiao
。这个嵌套函数有权访问其作用域中的变量,而且可以使用定义在外部函数中的变量counter
。外部函数一旦返回,就没有别的代码能够看到变量counter
了,此时内部函数拥有对它的专有访问权
类似counter
这样的私有变量并非只能由一个闭包独享。同一个外部函数中完全可以定义两个或更多嵌套函数,而它们共享相同的作用域。
这就厉害了,那不妨再看看下面这个例子:
function counter(){
let n = 0;
return{
count:function(){return n++};
reset:function(){n = 0}
};
}
let c = counter(),d=counter()
c.count() //0
c.count() //1
d.count() //0
c.reset()
c.conut() //0
d.count() //1
这个counter()
函数返回一个“计数器”对象。该对象有两个方法:count()
和reset()
。前者返回下一个整数,后者重置内部状态。
首先要理解的是,这两个方法都有权访问私有变量 n
。其次是要知道,每次调用counter()
都会创建一个新作用域(与之前调用创建的作用域相互独立),还有作用中域中的一个新私有变量。因此如果调用两次counter()
,就会得到拥有两个不同私有变量的计数器对象。在一个计数器上调用count()
或reset()
不会影响另一个计数器。
小总结
本文只是对闭包进行一个初体验,在了解闭包是什么的基础上浅浅地了解一下闭包的经典案例。明白这种函数对象与作用域(即一组变量绑定)组合起来解析函数变量的机制。当然闭包还有更多高级的用法,待我学成归来继续探索。