今天咱们来聊聊一个经典的面试题,也是很多新手容易踩坑的问题——在for循环中使用setTimeout。先看这段代码:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
你以为它会输出0,1,2,3,4?太天真了!实际输出是五个5!这是为什么?又该如何解决?且听我慢慢道来~
一、为什么会这样?——作用域与闭包的"陷阱"
这个现象背后隐藏着JavaScript的两个重要特性:
- var没有块级作用域:在for循环中用var声明的i实际上是函数作用域(或全局作用域)的
- 异步执行:setTimeout的回调函数会在循环结束后才执行
具体执行过程是这样的:
- for循环瞬间执行完毕(同步代码),i从0增加到5(当i=5时循环停止)
- 1秒后,5个setTimeout回调开始执行
- 此时它们访问的都是同一个i,而i的值已经是5了
- 所以输出了5个5
二、解决方案1:使用IIFE创建闭包
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
原理:
- 立即执行函数(IIFE)为每次循环创建一个新作用域
- 把当前的i值作为参数j传入并"冻结"住
- 每个setTimeout回调访问的都是自己闭包中的j
三、解决方案2:使用let块级作用域(ES6推荐)
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
这是最优雅的解决方案!
- let有块级作用域,每次循环都会创建一个新的i
- 相当于自动为我们创建了闭包
- 代码简洁直观,没有魔法
四、解决方案3:利用setTimeout的第三个参数
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(j);
}, 1000, i);
}
小技巧:
- setTimeout可以接受多个参数,第三个及以后的参数会作为回调函数的参数
- 相当于浏览器帮我们做了参数绑定
五、解决方案4:用bind提前绑定参数
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(j);
}.bind(null, i), 1000);
}
原理:
- Function.prototype.bind可以提前绑定参数
- 第一个参数是this(这里不需要所以传null)
- 后续参数会作为绑定函数的参数
六、深入理解:为什么let能解决问题?
let在for循环中的行为很特殊:
- 每次迭代都会创建一个新的词法环境(可以理解为新的作用域)
- 新的i会在这个环境中初始化,值为上一次迭代结束时的值
- 相当于自动为我们创建了闭包
可以近似理解为:
{
let i = 0;
setTimeout(function() { console.log(i); }, 1000);
}
{
let i = 1;
setTimeout(function() { console.log(i); }, 1000);
}
七、实际开发中的建议
- 默认使用let/const:告别var,拥抱块级作用域
- 注意异步代码的依赖关系:异步回调中使用循环变量时要特别小心
- 合理使用闭包:理解闭包的工作原理,但不要滥用
- 考虑代码可读性:有时候把异步逻辑提取成独立函数会更清晰
八、举一反三:类似的陷阱
这种问题不仅出现在setTimeout中,其他异步场景也会遇到:
var buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log(i);
});
}
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log(i);
});
}
九、总结
问题根源:var的作用域 + 异步执行时机
解决方案:
- IIFE创建闭包(传统方式)
- 使用let(最推荐)
- 利用setTimeout第三个参数
- 使用bind绑定参数
最佳实践:使用let/const避免这类问题
记住,在JavaScript中,同步代码和异步代码的执行时机是需要特别关注的重点。理解闭包和作用域,就能轻松应对这类问题。
转自https://juejin.cn/post/7510587921788321832
该文章在 2025/6/4 11:48:19 编辑过