结论先行:
因为 setTimeout 为宏任务,根据 JS 的事件循环机制,当主线程同步任务执行完成之后才会去执行宏任务。这时候 for 循环已经全部执行完毕,i 就是最后的值了。
解决办法:
① 使用立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上不会改变;当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j , 从而达到目的。
② 使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。原理类似于方法①
③ 使用 let 定义 i 【推荐】。因为 let 可以定义块级作用域,所以不存在变量覆盖的问题。
具体分析:
1、问题
首先,因为 setTimeout 是个异步函数,所以会先把 for 循环全部执行完毕,这时候 i 就是 6了,所以会输出 5 个 6
因为 setTimeout 为宏任务,由于JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
而且, setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6了,因此最后输出 5 个 6
for(var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i) // 打印6 6 6 6 6
}, i * 1000)
}
2、解决办法
① 利用 IIFE (立即执行函数)
使用立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上不会改变;当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j , 从而达到目的。
for(var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j) // 打印1 2 3 4 5
}, j * 1000)
})(i)
}
② 使用 setTimeout 的第三个参数
setTimeout 作为经常使用的定时器,它是存在第三个参数的。我们经常使用前两个,一个是回调函数,另外一个是定时时间,setTimeout 从第三个入参位置开始往后,是可以传入无数个参数的。
这些参数会作为回调函数的附加参数存在。
这个参数会被当成 timer 函数的参数传入
for(var i = 1; i <= 5; i++) {
setTimeout(function timer(j) {
console.log(j)
}, i * 1000, i)
}
③ 使用 let 定义 i 【推荐】
let 可以定义块级作用域,因为在异步的时候不存在变量覆盖的问题。
ES6 中新增的 let 定义变量的方式,使得 ES6 之后 JS 发生革命性的变化,让 JS 有了块级作用域,代码的作用域以块级为单位进行执行。
可以看到,通过 let 定义变量的方式,重新定义 i 变量,则可以用最少的改动成本,解决该问题。
for(let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}