谈谈 setTimeout 这道经典题目

谈谈本身对下面这道题目的理解html

问题

for (var i = 1; i <= 3; i++) { 
    setTimeout( function timer() {
        console.log(i);
    }, i * 1000 );
}

这段代码的输出是三次 4,与预想的 1,2,3 的输出不符。如下解释这一输出的缘由。前端

分析

咱们能够将 setTimeout 的第一个参数 timer() 单独写出来,变成以下代码:segmentfault

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,即:code

for (let i = 1; i <= 3; i++) { 
    function timer() {
        console.log(i);
    }
    setTimeout( timer, i * 1000 );
}

注意,因为 let 属于 ES6 的语法,请注意测试使用的浏览器。htm

此时,因为 let i = xxx 为块级别做用域,于是这一状况下的循环展开结果为:blog

{
    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. 前端基础进阶(四):详细图解做用域链与闭包 - 简书

相关文章
相关标签/搜索