对于那些有一点JavaScript使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,但是需要付出非常多的努力才能理解这个概念
闭包就像从JavaScript中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够到达那里
秘诀:JavaScript中闭包无处不在,你只需要能够识别并拥抱它
来看一段代码
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
bar(); // 2
这里bar()可以访问foo()的内部作用域,我们将bar()函数本身当作一个值类型进行传递。
foo()执行后,返回值赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()
bar()依然持有对该作用域的引用,而这个引用就叫做闭包
无论通过何种手段将内部函数传递到所在的词法作用域以外,他都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
本质上无论何时何地,如果将函数(访问他们各自的词法作用域)当作第一级的值类型并到处传递,你都会看到闭包在这些函数中的应用,在定时器,事件监听器,ajax请求,跨窗口通信,WebWorkers或者任何其他的异步(或同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
循环与闭包
接下来的几个例子,我希望你能理解并能给别人讲明白
for(var i = 1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},i*1000);
}
正常情况下,对这段代码的预期是分别输出数字1-5,每秒一个,每次一个
但是,这段代码的实际运行效果是输出**==5个6==**
6的来源肯定是for循环的结束条件产生的,i<=5退出的条件就是i=6
但是为什么是6呢?难道每个循环的i都不会保存吗?
- 循环中所有回调函数都会在循环结束时才执行,也就是setTimeout会在for结束时才执行
for(var i = 1;i<=5;i++)中i只有一个,因为他们都被封闭在一个共享的全局作用域中,所以用的都是同一个i
那,是不是我们给每个循环都单独开一个作用域就行了?
for(var i = 1;i<=5;i++){
(function(){
setTimeout(function timer(){
console.log(i);
},i*1000);
})();
}
看似我们通过IIFE创建了一个作用域,但是还是没用,结果依然输出了==5个6==
因为我们都IIFE只是一个什么都没有的空作用域,它需要包含实质内容才能为我们所用
for(var i = 1;i<=5;i++){
(function(j){
setTimeout(function timer(){
console.log(j);
},i*1000);
})(i);
}
这里我们把i作为参数给了我们的IIFE函数,好像,没问题了,输出是1-5。
因为在迭代中使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问,也就是变量j
不用IIFE有没有其他办法可以实现相同的效果?
我们先来看IIFE给我们解决了什么问题?
- 新建了一个单独的作用域,存放了正确的变量值
也没有其他方法可以实现相同的功能?
- 有,
let!!!
for(let i = 1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},i*1000)
}
上面的代码会输出1-5,和我们预想的效果一致(let大法好)
因为let会创建一个块作用域与当前的迭代循环中,可以理解为下面的代码
for (var i = 1;i<=5;i++){
let j = i;
setTimeout(() => {
console.log(j);
}, j*1000);
}
非常酷,非常快乐
有读者可能会想,我把let换成var行不行?
for (var i = 1;i<=5;i++){
var j = i;
setTimeout(() => {
console.log(j);
}, j*1000);
}
我在每个for循环中都定了一个变量j,看起来好像没啥问题?
好,运行
==5个5==
新的从未出现过的结果!
结果并非如我们所料般,全都输出了5,因为j被重复定义了,我们再来加一行代码,你也许就能明白为什么了
for (var i = 1;i<=5;i++){
var j = i;
setTimeout(() => {
console.log(j);
}, j*1000);
}
console.log(j); // new add
我们在结尾打印了j,成功输出了5,这个时候我们终于意识到了
- 噢,
j是定义在全局作用域里的,所以它被重复定义了
具体的let和var的区别,可以看这篇文章👉juejin.cn/post/692564…