前言
上回谈到 JS 的[作用域和作用域链](一个例子!浅谈作用域链 - 掘金 (juejin.cn)),我们理解了一些关于 JS 底层的逻辑思想。就是内部的函数可以访问到外部函数的变量和外部的函数无法访问到内部函数的变量,我们知道这与代码在执行过程中,执行期上下文的创建与销毁有关。当然这里‘函数’的叫法有所狭隘,我们更换为作用域。
但是今天我们说点不一样的,可以会推翻你对上面这段话的认知,我们将来实现外层作用域可以访问到内层作用域的变量。或许你会说,这不是改变了JS这门语言的特性了吗?这属于是出Bug吗?别急,他自然有他存在的道理和作用。
闭包(closure)
function foo(){
var a = 2;
function bar(){
console.log('a: ' + a); // ?
}
return bar;
}
var baz = foo();
baz();
思考上面代码的运行结果,比较意外,foo函数内的变量a 被打印出来了。按照之前的说法,foo函数在执行完成之后,内存就应该被 垃圾回收机制 回收,对应的执行期上下文也会被销毁,自然内部变量a也就不应该存在。现在a可以被访问到,就说明上面这些行为都没有发生。
我们可以看到foo函数在最后将bar函数对象本身作为返回值,并且在全局用baz变量接收并且执行,bar函数在自己作用域之外的地方执行了,并且外层foo函数的作用域依然存在(bar函数在执行时需要打印变量a,便会在作用域链上从上至下搜寻变量a,直至在foo函数的执行期上下文寻得并将其打印)
看到这里你觉得“神奇”的地方莫过于foo函数的作用域依然存在,并且是bar函数在使用。我们可以相信有这样一段对话:
垃圾回收机制:foo函数,你应该已经运行完成,可以交还使用的内存了。
foo函数:是这样的“垃圾回收机制”大哥,我确实已经执行完毕,但是我内部创建的bar函数被作为返回值传递了出去,他有没有运行完我无法保证。
垃圾回收机制:既然如此,就等bar函数运行完毕再说。
当然我们也可以从作用域链的角度思考问题,代码执行的过程中,bar函数的执行期上下文应该建立在foo函数的执行期上下文的上面,在变量检索的过程中,从作用域链的头部(bar函数的执行期上下文)向下检索,直至全局。如果foo函数的执行期上下文被销毁,bar函数的执行期上下文将会脱离作用域链,进而失去对其的控制。
对于上面这种现象,及一个函数被保存到外部,就会产生闭包。 这也属于是 JS 这门语言的一种现象,我们可以利用这样的现象,来实现一些效果。
function a() {
let i = 0;
function b() {
i++;
console.log(i); // ?
}
return b;
}
var c = a();
for (let i = 0; i < 5; i++) {
c();
}
c函数被for循环了五次,最后分别打印了 1,2,3,4,5 五个数字。我们利用闭包的思想来理解一下,a函数执行完将b函数对象本身传递给了变量c,c函数执行,也就是b函数执行,使用了a函数的作用域,变量i始终存在,b函数的i++操作就使得变量i随着函数的执行一次就增加一。再依次打印出来就是1,2,3,4,5 五个数字的结果。这样就实现了每调用一次c函数就可以让变量i增加一的功能,及计数的功能。
作用域和闭包
function foo(){
for(var i=0; i<6;i++){
setTimeout(function (){
console.log(i); // ?
},1000 * i);
}
}
foo();
思考上面这段代码,结果再次出乎我们的意料,以**每秒一次的频率输出六个6** 。
我们预期的结果应该是每秒一次的频率输出 0,1,2,3,4,5 。
是什么导致了预期与结果不一样呢?我们思考一下代码的运行,for循环执行很快,几乎同时设定好了六个倒计时分别为1秒,2秒,3秒,... ,6秒的计时器,此时变量i为6跳出循环,计时器几乎同时开始倒计时,第一个1秒结束,计时器的回调函数需要打印变量i,此时变量i为6,于是他打印出了第一个6,一秒钟之后,第二个1秒结束,计时器的回调函数需要打印变量i,此时变量i为6,于是他打印出了第二个6,... 依此类推。
现在要思考一个问题,我们应该如何操作让代码可以打印出我们预期的结果?
我们想到了上面第二个计数的代码,每次执行变量i都是不一样的。那么我们是不是也可以 让回调函数引用一个作用域 ,在 作用域里存放在每次循环变量i的值,这样打印的就是这个作用域里变量的值,而不是最后变为6的变量i,没错我们已经想到要用上今天介绍的闭包。
function foo(){
for(var i=0; i<6;i++){
(function(){
var j = i
setTimeout(function (){
console.log(j); // ?
},1000 * i);
})();
}
}
foo();
我们使用一个 立即执行函数来包含计时器,立即执行函数也会产生一个函数作用域,在这个作用域里我们再用一个 变量j来存放每次循环变量i的值。for循环结束之后,里面的立即执行函数所创建的执行期上下文并不会被立即销毁,因为里面的回调函数并没有运行,这样看来,是不是就形成了闭包。当回调函数执行时,所打印的值便是每一次for循环所保存下来的变量i的值。
由此可见,作用域和闭包可以解决一些我们意料之外的代码情况。闭包并不是为了破坏作用域规则而存在的,他使得作用域的使用更加广泛。
总结
函数嵌套函数
内层函数引用外层函数变量,内层函数对象被创建,闭包就形成了。