谈谈自己对下面这道题目的理解
问题
for (var i = 1; i <= 3; i++) {
setTimeout( function timer() {
console.log(i);
}, i * 1000 );
}
这段代码的输出是三次 4,与预想的 1,2,3 的输出不符。以下解释这一输出的原因。
分析
我们可以将 setTimeout
的第一个参数 timer()
单独写出来,变成如下代码:
for (var i = 1; i <= 3; i++) {
function timer() {
console.log(i);
}
setTimeout( timer, i * 1000 );
}
然后我们将循环展开,三次执行过程的变化如下:
// 第一步: i = 1;
setTimeout( timer, 1 * 1000 );
// 第二步:i = 2;
setTimeout( timer, 2 * 1000 );
// 第三步 i = 3;
setTimeout( timer, 3 * 1000 );
注意,在循环过程中,timer()
函数并未变化,也没有执行( 计时器还未开始 )。
由于 JavaScript 中使用 var i = xxx
声明的变量是函数级别( 而非块级 )的作用域,因而在 for 循环条件中声明的 i
在 for 循环块之外的最后一个函数体内仍是可以访问的,循环可以展开为:
var i = 4;
function timer() {
console.log(i);
}
setTimeout( timer, 1 * 1000 );
setTimeout( timer, 2 * 1000 );
setTimeout( timer, 3 * 1000 );
因而当计时器开始的 1s, 2s, 3s 后,timer 会分别执行,此时会输出三次 4。
解决方法
若要其每隔 1s 分别输出 1, 2, 3,可以将 var i = 1
修改为 let i = 1
,即:
for (let i = 1; i <= 3; i++) {
function timer() {
console.log(i);
}
setTimeout( timer, i * 1000 );
}
注意,由于 let
属于 ES6 的语法,请注意测试使用的浏览器。
此时,由于 let i = xxx
为块级别作用域,因而这一情况下的循环展开结果为:
{
let i = 1;
setTimeout( timer, 1 * 1000 );
}
{
let i = 2;
setTimeout( timer, 2 * 1000 );
}
{
let i = 3;
setTimeout( timer, 3 * 1000 );
}
注意:这里的 {}
仅用来强调块级别作用域。
此时便可以得到我们想要的输出结果了。
此外,还可以使用下面这种方式:
for (var i = 1; i <= 3; i++) {
(function(count){
setTimeout( function timer() {
console.log(count);
}, count * 1000 );
})(i)
}
这里可以使用闭包的知识进行解释( 有关闭包的内容可以参见文末的参考链接 ),也可以用作用域辅助理解。
由于 var i = xxx
是函数级别作用域,这里通过一个立即函数将变量 i
传入其中,使其包含在这一函数的作用域中。而在每次循环中,此立即函数都会将传入的 i
值保存下来,因而其循环展开结果为:
(function(){
var count = 1;
setTimeout( function timer() {
console.log(count);
}, count * 1000 );
})()
(function(){
var count = 2;
setTimeout( function timer() {
console.log(count);
}, count * 1000 );
})()
(function(){
var count = 3;
setTimeout( function timer() {
console.log(count);
}, count * 1000 );
})()
自然也会得到我们想要的输出结果。
扩展 - 块级作用域和函数级作用域
可以用以下代码进行解释:
{
let i = 2;
// 输出 2
console.log(i);
}
// 报错:Uncaught ReferenceError: i is not defined
console.log(i);
function test(){
// 由于变量提升,输出 undefined
console.log(a);
{
var a = 1;
}
// 输出 1
console.log(a);
}
// 按照函数内的注释输出
test();
// 报错:Uncaught ReferenceError: a is not defined
console.log(a);
注:const
声明的常量与 let
相同,也为块级作用域。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。