资讯专栏INFORMATION COLUMN

谈谈 setTimeout 这道经典题目

Sleepy / 2180人阅读

摘要:谈谈自己对下面这道题目的理解问题这段代码的输出是三次,与预想的,,的输出不符。此外,还可以使用下面这种方式这里可以使用闭包的知识进行解释有关闭包的内容可以参见文末的参考链接,也可以用作用域辅助理解。

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

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

参考

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

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

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81514.html

相关文章

  • JavaScript系列——JavaScript同步、异步、回调执行顺序之经典闭包setTimeou

    摘要:同步异步回调傻傻分不清楚。分割线上面主要讲了同步和回调执行顺序的问题,接着我就举一个包含同步异步回调的例子。同步优先回调内部有个,第二个是一个回调回调垫底。异步也,轮到回调的孩子们回调,出来执行了。 同步、异步、回调?傻傻分不清楚。 大家注意了,教大家一道口诀: 同步优先、异步靠边、回调垫底(读起来不顺) 用公式表达就是: 同步 => 异步 => 回调 这口诀有什么用呢?用来对付面试的...

    lewif 评论0 收藏0
  • JavaScript系列——JavaScript同步、异步、回调执行顺序之经典闭包setTimeou

    摘要:同步异步回调傻傻分不清楚。分割线上面主要讲了同步和回调执行顺序的问题,接着我就举一个包含同步异步回调的例子。同步优先回调内部有个,第二个是一个回调回调垫底。异步也,轮到回调的孩子们回调,出来执行了。 同步、异步、回调?傻傻分不清楚。 大家注意了,教大家一道口诀: 同步优先、异步靠边、回调垫底(读起来不顺) 用公式表达就是: 同步 => 异步 => 回调 这口诀有什么用呢?用来对付面试的...

    rockswang 评论0 收藏0
  • 前端er,你真的会用 async 吗?

    摘要:异步函数是值通过事件循环异步执行的函数,它会通过一个隐式的返回其结果。 async 异步函数 不完全使用攻略 前言 现在已经到 8012 年的尾声了,前端各方面的技术发展也层出不穷,VueConf TO 2018 大会 也发布了 Vue 3.0的计划。而在我们(我)的日常中也经常用 Vue 来编写一些项目。那么,就少不了 ES6 的登场了。那么话说回来,你真的会用 ES6 的 asyn...

    Jaden 评论0 收藏0
  • 4道经典指针笔试题讲解 ~

    摘要:结尾有关这四道经典的指针笔试题讲解就到此结束了,如果觉得文章对自己有所帮助,欢迎大家多多点赞收藏 ?前言 : 今天博主来讲解4道经典的指针笔试题,很多朋友没有深刻理...

    tianren124 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<