LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

javascript篇:setTimeout遇上for循环:为什么总是输出5?如何正确输出0-4?

freeflydom
2025年6月4日 11:39 本文热度 270

今天咱们来聊聊一个经典的面试题,也是很多新手容易踩坑的问题——在for循环中使用setTimeout。先看这段代码:

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

你以为它会输出0,1,2,3,4?太天真了!实际输出是五个5!这是为什么?又该如何解决?且听我慢慢道来~

一、为什么会这样?——作用域与闭包的"陷阱"

这个现象背后隐藏着JavaScript的两个重要特性:

  1. var没有块级作用域:在for循环中用var声明的i实际上是函数作用域(或全局作用域)的
  2. 异步执行:setTimeout的回调函数会在循环结束后才执行

具体执行过程是这样的:

  1. for循环瞬间执行完毕(同步代码),i从0增加到5(当i=5时循环停止)
  2. 1秒后,5个setTimeout回调开始执行
  3. 此时它们访问的都是同一个i,而i的值已经是5了
  4. 所以输出了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循环中的行为很特殊:

  1. 每次迭代都会创建一个新的词法环境(可以理解为新的作用域)
  2. 新的i会在这个环境中初始化,值为上一次迭代结束时的值
  3. 相当于自动为我们创建了闭包

可以近似理解为:

// 伪代码,帮助理解let的行为
{
  let i = 0;
  setTimeout(function() { console.log(i); }, 1000);
}
{
  let i = 1;
  setTimeout(function() { console.log(i); }, 1000);
}
// ...以此类推

七、实际开发中的建议

  1. 默认使用let/const:告别var,拥抱块级作用域
  2. 注意异步代码的依赖关系:异步回调中使用循环变量时要特别小心
  3. 合理使用闭包:理解闭包的工作原理,但不要滥用
  4. 考虑代码可读性:有时候把异步逻辑提取成独立函数会更清晰

八、举一反三:类似的陷阱

这种问题不仅出现在setTimeout中,其他异步场景也会遇到:

// 事件监听中的类似问题
var buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', function() {
    console.log(i); // 总是输出buttons.length
  });
}
// 解决方案同样适用
for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', function() {
    console.log(i); // 正确输出对应的索引
  });
}

九、总结

  1. 问题根源:var的作用域 + 异步执行时机

  2. 解决方案

    • IIFE创建闭包(传统方式)
    • 使用let(最推荐)
    • 利用setTimeout第三个参数
    • 使用bind绑定参数
  3. 最佳实践:使用let/const避免这类问题

记住,在JavaScript中,同步代码和异步代码的执行时机是需要特别关注的重点。理解闭包和作用域,就能轻松应对这类问题。

转自https://juejin.cn/post/7510587921788321832


该文章在 2025/6/4 11:48:19 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved