先来看看下面这一道题吧:
//下面这段代码的执行结果是什么,为什么?
for (var i = 0; i < 3; i++) {
setTimeout(function (){
console.log(i);
})
}
//打印结果
//3
//3
//3
有一定前端基础的应该都知道结果是打印三个3,也知道这跟作用域有关,但具体是引发这个问题的呢。以下是我个人的理解,如有问题,欢迎指正。
首先,我们需要先搞清楚for循环具体是怎样实现的:
//这是一个for循环
for (var i = 0; i < 3; i++) {
console.log(i);
}
console.log(i);
//执行结果:
//0
//1
//2
//3
//这个for循环实际的执行过程应该是这样的
{
var i=0;
console.log(i);
i++;
}
{
var i=1;
console.log(i);
i++;
}
{
var i=2;
console.log(i);
i++;
}
console.log(i);
//执行结果:
//0
//1
//2
//3
由于var的作用域是函数作用域,且var可以重复声明同名变量并覆盖,因此在这段代码中for循环执行结束后,i的值任然存在,所以最终会打印3。又由于就近原则,console.log(i)执行时,会找当前作用域中最近声明的那一个i的值,因此打印0,1,2,3。
正如前面所说的那样,var的作用域是函数作用域,且可以重复声明,了解了var的这两个特性之后,一开始的那个问题也就好理解了:
for (var i = 0; i < 3; i++) {
setTimeout(function (){
console.log(i);
})
}
//执行结果:
// 3
// 3
// 3
//这个for循环的实际执行过程应该是这样的
{
// var i=0;这个i在被实际引用之前就已经被var i=2给覆盖了,因此声明了也没有实际作用
setTimeout(function (){
console.log(i);
});
i++;
}
{
// var i=1;这个i在被实际引用之前就已经被var i=2给覆盖了,因此声明了也没有实际作用
setTimeout(function (){
console.log(i);
});
i++;
}
{
var i=2;
setTimeout(function (){
console.log(i);
});
i++;
}
//到这里同步任务执行完毕,i=3
//执行结果:
// 3
// 3
// 3
上面的console.log(i)会被放入异步任务队列中等待同步任务执行完毕后再执行,前面声明的i还没有被引用就被var i=2给覆盖了。同步任务执行完毕后,全局的i值为3,console.log(i)在块级作用域找不到i就会去外层的作用域找,直到在该函数或全局内找不到,于是打印的都是3。
解决这个问题常用的方法就是不用var,如果开发环境有限制的话,那就可以使用闭包。以下是闭包的方案:
for (var i = 0; i < 3; i++) {
(function foo(m) {
setTimeout(function () {
console.log(m);
})
})(i)
}
//执行结果:
// 0
// 1
// 2
//这个for循环的实际执行过程应该是这样的
{
var i = 0;
(function foo(m) {
var i=m;
setTimeout(function () {
console.log(i);
})
})(i)
i++;
}
{
var i = 1;
(function foo(m) {
var i=m;
setTimeout(function () {
console.log(i);
})
})(i)
i++;
}
{
var i = 2;
(function foo(m) {
var i=m;
setTimeout(function () {
console.log(i);
})
})(i)
i++;
}
//执行结果:
// 0
// 1
// 2
将每次for循环声明的i以参数的形式传给一个函数(foo)并执行,当前for循环声明的i的值就保存在了foo函数的内部,即便后续声明的i把现在的i值覆盖了,也不会影响到foo内部的i的值。
当然,使用let和const是最简单的方法,它们俩是块级作用域。