前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库var 循环变量闭包异步回调 修复
JavaScript函数与作用域

for 循环里用 var,setTimeout 为什么打印出来全是同一个数?

var 声明的 i 整个函数只有一个,三个回调读的是同一个 i。回调要等循环跑完才执行,这时 i 已经是 3 了。把 var 换成 let 就好,let 每轮循环都会生成一个新的 i。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#函数与作用域#异步编程
先看核心答案读代码示例
理解线索

一个 `i` 还是三个 `i`

  1. var整个函数一个 i,三个回调共用,最后都读到 3
  2. let每轮循环一个新的 i,三个回调各拿各的
  3. IIFE每轮调用一次函数,把当时的 i 当参数存进去

问题不在 setTimeout,换成 Promise.then 或点击事件回调也一样。只要回调是循环结束后才执行的,var 就会出这个问题。

核心回答

先记住这个答案

因为 var 声明的 i 在整个函数里只有一个,三个回调读的是同一个 i。setTimeout 的回调要等循环全部跑完才执行,这时 i 已经加到 3 了,所以打印三个 3。最简单的修法是把 var 换成 let 就行。let 在每轮循环都会生成一个新的 i,每个回调记住的是自己那一轮的 i。

  • var 只有一个 i,所有回调共用
  • 回调执行时循环已结束,i 已经是最终值
  • 闭包记住的是变量本身,不是当时的值
  • 修法:let、IIFE、setTimeout 第三个参数

先看代码和输出

这是面试里最常出现的写法,先猜一下输出。

var 和 let 的对比JavaScript
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log('var', i))
}

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log('let', j))
}
查看输出与解释
var 3
var 3
var 3
let 0
let 1
let 2

执行顺序是这样的:两个循环先同步跑完,只是把 6 个回调放进了定时器队列;等同步代码结束,回调才依次执行。第一个循环的 3 个回调去读 i,只有一个 i,而且已经是 3。第二个循环每轮都有自己的 j,分别是 0、1、2。

为什么闭包读到的是最终值

很多人以为回调创建时会把 i 的值「拍个照」存下来,其实不是。闭包记住的是变量本身,等回调真正执行时,才去读这个变量现在的值。

所以关键就两点:有几个 i,以及什么时候去读。var 只有一个 i,读的时候循环已经结束了,所以全是 3。如果回调在循环里同步立刻执行,var 也能打印出 0、1、2,因为读的时候 i 还没变。

let 能解决,是因为 for 循环对 let 有特殊处理:每进入新的一轮,就用上一轮的值新建一个 j。三个回调各自指向不同的 j,互不影响。

不用 let 的两种修法

老项目或者面试官要求不用 let 时,思路都是一样的:想办法让每轮循环有一个自己的变量。

用 IIFE 把当时的 i 存成参数JavaScript
for (var i = 0; i < 3; i++) {
  ;(function (n) {
    setTimeout(() => console.log(n)) // 读的是参数 n,不是外面的 i
  })(i)
}
// 0 1 2

每轮循环都立刻调用一次函数,把当时的 i 作为参数 n 传进去。函数每调用一次就有一个新的 n,回调读的是 n,自然就是 0、1、2。

用 setTimeout 的第三个参数
setTimeout 从第三个参数开始,会原样传给回调。写成 setTimeout((n) => console.log(n), 0, i),调用 setTimeout 那一刻 i 的值就被当作参数传进去了,效果和 IIFE 一样。这个写法面试时提一句,能看出你对 API 比较熟。
回答前,多想一步

容易答错的地方

以为是 setTimeout 的问题
setTimeout 只是让回调晚点执行,真正的原因是只有一个 i。换成 Promise.resolve().then()、按钮点击回调,只要是循环结束后才执行,结果都一样。
以为 let 会把整个循环体都复制一份
let 只是让循环变量每轮都是新的。循环外面定义的对象、数组还是只有一份,在回调里修改它们,所有回调看到的都是同一个。
试着用自己的话回答

面试官还会怎么问?

如果把 setTimeout 的延迟改成 i * 1000,输出会变吗?

输出还是三个 3,只是每隔一秒打印一个。延迟时间是在调用 setTimeout 时就算好的,分别是 0、1000、2000 毫秒,但回调里读 i 的时机还是在循环结束之后。

forEach 为什么没有这个问题?

forEach 每处理一个元素都会调用一次回调函数,每次调用都有自己的参数,相当于天然用了 IIFE 的写法,不存在共用一个变量的问题。

在 let 循环体里修改 j,会影响下一轮吗?

会。下一轮的 j 是用本轮结束时的值再加 1 生成的,本轮把 j 改成 5,下一轮就从 6 开始。但已经创建的回调记住的还是本轮那个 j。

从一道题,走向一组知识

把知识连起来

函数与作用域

闭包为什么会导致内存泄漏?怎么避免?

闭包会一直留住它引用的变量,这也是闭包导致内存泄漏的原因。

函数与作用域

柯里化是怎么用闭包实现的?能手写一个 curry 吗?

柯里化靠闭包把前面传的参数存起来,和 IIFE 修法是同一个原理。

参考资料

  • JavaScript:函数与作用域参考资料

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 先看代码和输出
  3. 为什么闭包读到的是最终值
  4. 不用 let 的两种修法
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑