for循环 + setTimeout 结合的烂大街的面试题

1、背景

最近在翻看之前的老书《node.js开发指南》,刚好碰到 for 循环 + setTimeout 的经典例子,因而从新梳理了思路并记录下。node

2、写在前面,setTimeout 和 setInterval 的执行机制

在平常编码中,你会发现,给 setTimeout 和 setInterval 设定延迟时间每每并不许,或者干脆 setTimeout(function(){xxx},0) 也不是立马执行(特别是有耗时代码在前),这是由于 js 是单线程的,有一个事件队列机制,setTimeout 和 setInterval 的回调会到了延迟时间塞入事件队列中,排队执行。异步

setTimeout :延时 delay 毫秒以后,啥也无论,直接将回调函数加入事件队列。
setInterval :延时 delay 毫秒以后,先看看事件队列中是否存在尚未执行的回调函数( setInterval 的回调函数),若是存在,就不要再往事件队列里加入回调函数了。函数

看下面示例:编码

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

结果:1 秒以后,同时输出 5 个 5。线程

由于 for 循环会先执行完(同步优先于异步优先于回调),这时五个 setTimeout 的回调所有塞入了事件队列中,而后 1 秒后一块儿执行了。code

3、正文

接下来就是那道经典的代码:队列

for (var i = 0; i < 5; i++) { 
    setTimeout(function (){
        console.log(i);  
     },1000);  
}

结果:5 5 5 5 5事件

为何不是 1 2 3 4 5,问题出在做用域上。作用域

由于 setTimeout 的 console.log(i); 的i是 var 定义的,因此是函数级的做用域,不属于 for 循环体,属于 global。等到 for 循环结束,i 已经等于 5 了,这个时候再执行 setTimeout 的五个回调函数(参考上面对事件机制的阐述),里面的 console.log(i); 的 i 去向上找做用域,只能找到 global下 的 i,即 5。因此输出都是 5。开发

解决办法:人为给 console.log(i); 创造做用域,保存i的值。

解决办法一

for (var i = 0; i < 5; i++) { 
    (function(i){      //马上执行函数
        setTimeout(function (){
            console.log(i);  
         },1000);  
    })(i);  
}

这里用到马上执行函数。这样 console.log(i); 中的i就保存在每一次循环生成的马上执行函数中的做用域里了。

解决办法二

for (let i = 0; i < 5; i++) {     //let 代替 var
    setTimeout(function (){
        console.log(i);  
     },1000);  
}

let 为代码块的做用域,因此每一次 for 循环,console.log(i); 都引用到 for 代码块做用域下的i,由于这样被引用,因此 for 循环结束后,这些做用域在 setTimeout 未执行前都不会被释放。

4、补充

在写示例代码的过程当中,发现一个语法点:

function a(i){ 
    console.log(i);   
 }

for (var i = 0; i < 5; i++) { 
    setTimeout(a(i),1000); 
}

报错:

TypeError: "callback" argument must be a function
at setTimeout (timers.js:421:11)
……

百度了下,原来 setTimeout 不支持传带参数的函数,有两种解决方案:

(1)匿名函数包装

function a(i){ 
    console.log(i);   
}

for (var i = 0; i < 5; i++) { 
    setTimeout(function(){  //用匿名函数包装
        a(i);
    },1000); 
}

(2)setTimeout 的第 3+ 个参数

setTimeout(func, delay, param1, param2, ...)
第三个参数及之后的参数均可以做为 func 函数的参数

function a(i){ 
    console.log(i);   
 }

for (var i = 0; i < 5; i++) { 
    setTimeout(a,1000,i);   //传入第3个参数
}
相关文章
相关标签/搜索