你以为 setTimeout 是异步的就安全了?这题 5 个 5,面试官笑了

272 阅读2分钟

你以为 setTimeout 是异步的就安全了?这题 5 个 5,面试官笑了

初看一眼觉得没毛病,执行后却一脸问号?
这是一道 JavaScript 面试经典题,但隐藏着作用域和闭包的双重陷阱。

👇 面试题目

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

你猜输出结果是?

❌ 不是 0, 1, 2, 3, 4

✅ 而是:

5
5
5
5
5

🤯 为什么会这样?明明是异步的!

我们得从两个知识点讲起:

🔍 1. var 的作用域特性

  • var函数作用域,不是块级作用域。
  • 所以 for 循环里的 i 是在同一个作用域里共享的
  • setTimeout 在 1 秒后执行时,for 早就跑完了,i 已经变成了 5。

每一个箭头函数都闭包捕获了同一个变量 i,最终输出的都是 5


🔗 2. JavaScript 的作用域链与闭包

📌 闭包定义:

闭包是函数和其**词法作用域(定义时作用域)**之间的组合。即使函数在外部执行,它也能访问它定义时的作用域。

所以这段代码里,每个 setTimeout 捕获的是同一个 i 的引用,而不是值。


✅ 正确写法:如何输出 0~4?

我们有两种方式来修复它:

✅ 方法一:使用 let(推荐)

for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 1000);
}
💡 为什么有效?
  • let块级作用域
  • 每次循环都会创建一个新的作用域,每个 i 都是独立的。
  • 每个 setTimeout 闭包都捕获了对应的 i 值。

✅ 方法二:使用 IIFE(立即执行函数)

for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 1000);
  })(i);
}
💡 原理解释:
  • 利用闭包将每次循环的 i 值通过参数传给一个新作用域 j,从而“固定”下来。

🎯 延伸:闭包 + 异步,是 JavaScript 的大坑

结合这道题,我们掌握了几个重要知识点:

知识点说明
作用域var 是函数作用域,let/const 是块级作用域
闭包函数可以访问它定义时的作用域
作用域链查找变量时按层层作用域往外找
异步执行setTimeout 是异步的,但它捕获的是变量引用,不是值

🧪 思维加餐:再来个变化题你能答对吗?

for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), i * 1000);
}

猜猜它输出什么?
👉 会每秒输出一次 0 1 2 3 4,延迟时间也变成了动态的。


🚀 总结

JavaScript 中的闭包和作用域链常常和异步结合成“送命题”。
弄清楚变量的作用域和函数的定义位置,就能掌握闭包的真正威力!


💬 最后

这题你答对了吗?是否在某次面试中遇到过类似的陷阱?欢迎留言交流!