闭包
说到作用域闭包,我想很多同学都知道,但是让你讲讲其原理以及应用场景,也许又不知从何说起。
其实作用域闭包无处不在,只是你自己没意识到。
简单来说,函数能够记住并可以访问所在的词法作用域时,便产生了闭包。看过<<JavaScript 高级程序设计>>的同学也可能会这样说,闭包就是定义在函数里的函数喽。其实这两种解释是一个意思,为什么这么说呢,我们先来看段代码
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
var test = foo();
test();//输出结果为2
看完这段代码你有发现什么问题吗?函数bar竟然在其所在的作用域外部被调用了。按道理来说,bar涵盖了foo定义的作用域,也只能在函数内部被调用。之所以在外部能被调用,是因为首先将bar标识符当做变量被foo返回,当 foo() 执行后,bar便赋值给了test,test的调用就是对bar的调用,这个过程实质上就是通过不同的标识符引用调用了内部的bar。
当 foo() 执行后,理论上内部的作用域是要被销毁的,因为引擎有垃圾回收器通过标记清除来回收不再使用的内存空间。但事实上,foo的内部作用域一直存在,被谁占用着呢?就是这个内部函数bar(),拜它声明的位置所赐,它依然保持着对foo内部作用域的引用,这个引用其实就是闭包。因此,后续的 test 才能够执行,并能够访问其所在作用域中的变量a。
其实说白了,如果将函数作为参数并到处传递,其所涵盖的作用域(例如其所在的函数构建的作用域)就一直在那里。所以像定时器、事件监听器、Ajax请求等,只要应用了回调函数的地方,我们都可以认为是在使用闭包。
循环和闭包
说到闭包,另一个典型的例子就是for循环了,看下面这段代码:
for( var i = 1; i <= 5; i ++){
setTimeout(function(){
console.log(i);
},i*1000);
}
上面代码会输出什么结果呢?是 1 2 3 4 5吗?不是的,其实是以每1秒的频率输出5次6,这是因为第一、延迟函数 setTimeout 里面定义的回调函数必须等到循环结束时才会调用,这个时候 i 已经变成6,;第二、每一次循环都会定义一个延迟函数,这样的话回调函数要调用5次。
不过,我们的真实的目的是想按顺序输出数字对吧,怎么办呢?有人可能很快想到了用作用域包起来啊,聪明的你怎么写代码呢?
for( var i = 1; i <= 5; i++){
(function(){
var j = i;
setTimeout(function(){
console.log(i);
},i*1000);
})();
}
仅仅是包起来还不够,还要添加点代码。在每个作用域内部定义一个属于自己的变量 j,这样,任你外部的 i 再怎么变化,我已经有了自己的专属变量 j,这样就可以达到目的了。
当然,代码还可以改成下面这样:
for( var i = 1; i <= 5; i ++){
(function(){
setTimeout(function(j){
console.log(i);
},i*1000);
})(i);
}
回到块作用域
我前面的文章有提到过块作用域,其中ES6新引入的 let 就可以劫持块作用域,并在这个作用域中声明一个变量。本质上就是将块作用域转化成封闭的作用域了。因此,我们可以写出下面的代码,照样能够按顺序输出数字。
for( var i = 1; i <= 5; i++){
let j = i;
setTimeout(function(){
console.log(j);
},i*1000);
}
当然,还有一种情形就是for循环的头部用let 声明(如下代码),这样它会被赋予一种特殊行为,就是每次循环 ,i 都会被声明一次,然后都会用上一次迭代结束的值赋予给 i,所以同样可以达到目的。
for( let i = 1; i <= 5; i++){
setTimeout(function(){
console.log(i);
},i*1000);
}
是不是觉得很酷、很神奇呢,块作用域和闭包联手起来原来这么厉害啊。
网友评论