setTimeout()基础
setTimeout 函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。
var timerId = setTimeout(func|code, delay)
上面代码中,setTimeout 函数接受两个参数,第一个参数 func|code 是将要推迟执行的函数名或者一段代码,第二个参数 delay 是推迟执行的毫秒数。
需要注意的是,推迟执行的代码必须以字符串的形式,放入 setTimeout,因为引擎内部使用 eval 函数,将字符串转为代码。如果推迟执行的是函数,则可以直接将函数名,放入 setTimeout。一方面 eval 函数有安全顾虑,另一方面为了便于 JavaScript 引擎优化代码,setTimeout 方法一般总是采用函数名的形式,就像下面这样。
function func(){
console.log(2);
}
setTimeout(func,1000);
// 或者
setTimeout(function (){console.log(2)},1000);
分析 for 循环中的 setTimeout 函数
let i = 0
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},3000)
}
说起事件循环,不得不提起任务队列。事件循环只有一个,但任务队列可能有多个,任务队列可分为宏任务(macro-task)和微任务(micro-task)。注意进入到任务队列的是具体的执行任务的函数。比如上述例子 setTimeout()中的 console.log 函数。
XHR 回调、事件回调(鼠标键盘事件)、setImmediate、setTimeout、setInterval、indexedDB 数据库操作等 I/O 以及 UI rendering 都属于宏任务(也有文章说 UI render 不属于宏任务,目前还没有定论),process.nextTick、Promise.then、Object.observer(已经被废弃)、MutationObserver(html5 新特性)属于微任务。
另外不同类型的任务会分别进入到他们所属类型的任务队列,比如所有 setTimeout()的回调都会进入到 setTimeout 任务队列,所有 then()回调都会进入到 then 队列。当前的整体代码我们可以认为是宏任务。事件循环从当前整体代码开始第一次事件循环,然后再执行队列中所有的微任务,当微任务执行完毕之后,事件循环再找到其中一个宏任务队列并执行其中的所有任务,然后再找到一个微任务队列并执行里面的所有任务,就这样一直循环下去。
再回过头来看上面那个问题,理解了事件循环的机制,问题就很简单了。for 循环时 setTimeout()不是立即执行的,它们的回调被 push 到了宏任务队列当中,而在执行任务队列里的回调函数时,变量 i 早已变成了 6。
让变量 i 执行看起来符合人们的逻辑
-
利用 ES 6 引入的 let 关键字
for(let i = 0;i<6;i++) { setTimeout(function timer(){ console.log(i); }, i * 1000); }
for 循环头部的 let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
-
用闭包
let i = 0 for(i = 0;i<6;i ++) { (function(i){ setTimeout(function timer() { console.log(i) }, i * 1000); })(i); }
利用函数创建一个局部变量,保留了调用时的局部变量 i。
- 利用 bind 函数
如果 bind 方法的第一个参数是 null 或 undefined,等于将 this 绑定到全局对象,函数运行时 this 指向顶层对象(浏览器为 window)。bind 还可以接受更多的参数,将这些参数绑定原函数的参数。let i = 0; for (i=0; i<6; i++) { setTimeout( function timer(i) { console.log(i); }.bind(null,i), i*1000 ); }
-
利用 setTimeout 第三个参数
let i = 0; for (i=1; i<6; i++) { setTimeout( function timer(i) { console.log(i); }, i*1000,i); }
setTimeout 函数第三个参数及以后的参数都可以作为 timer 函数的参数。