题目 12 / 14

为什么三个 setTimeout 回调都打印 3,而不是 0、1、2?

知识点: 闭包

var 在整个循环中只创建一个共享绑定,所有回调最终都会读到同一个最终值。

使用执行可视化器解题在下方逐步观察每个绑定、函数调用和控制台输出。

预测输出结果

预测输出。每次循环迭代会给回调一个独立的 i,还是三个回调共享同一个?

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 100);
}
查看答案与完整解析

正确输出

3
3
3

为什么会这样

var i 在整个循环期间只声明一次,而不是每次迭代各声明一次,因此传给 setTimeout 的三个回调闭包捕获的都是同一个 i 绑定。

循环会同步执行完毕,把共享的 i 递增到 3,并把三个回调都注册进定时器任务队列,此时它们都还没有机会运行。

同步代码执行完毕后,排队的回调依次运行。每个回调都在共享的外层作用域中查找 i,得到的都是同一个最终值 3。

这道题考察什么

  • var 是函数作用域,而非块级作用域
  • 闭包捕获的是绑定,而不是值的快照
  • 排队的回调只会在同步代码结束后运行

运行环境前提

可视化器把 setTimeout 建模为宏任务,总是在当前同步代码执行完毕后才运行,这与 Node.js、浏览器以及其他标准 JavaScript 运行时一致。把 var 换成 let 会让每次迭代拥有独立绑定,输出将变为 0、1、2。

规范参考

JavaScript 代码编辑器支持直接编辑或粘贴自定义代码
第 0 步 / 共 0 步
单步Space播放
按 → 单步 或 Space 自动播放
阶段 1: 编译预解析(变量提升与内存分配)
阶段 2: 代码执行(逐行赋值与调用求值)
单步执行原理解析

按键盘右方向键 “→” 进行单步调试,或按空格键 “Space” / 点击 “▶ 自动播放” 进行连续执行。

作用域链 (Scope Chain)
词法作用域链: 当前 AO → 外部 [[Scope]] → 全局 (GO)
暂无激活的作用域链
调用栈 (CALL STACK)执行上下文栈 (后进先出 LIFO)
后进先出 (LIFO) 栈帧
全局执行上下文 (GEC)
▶ 活动栈帧
堆内存空间 (HEAP & MEMORY SPACE)已分配对象 (GO, AO, FunctionObjects, ER)
ECMAScript 堆内存区
内存中暂无分配对象
控制台输出 (Console)
console.log(...) Stream
暂无输出产生