2

谈谈自己对下面这道题目的理解

问题

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 相同,也为块级作用域。


参考

  1. for 循环中的...问题,为什么改 var 为 let 就可以解决? - segmentfault

  2. ES6之let(理解闭包)和const命令 - 博客园

  3. 「每日一题」JS 中的闭包是什么? - 知乎专栏

  4. 前端基础进阶(四):详细图解作用域链与闭包 - 简书


dailybird
1.1k 声望73 粉丝

I wanna.