javascript

for 循环里 var 和 let 为什么结果不同

用经典的 `for` 循环闭包例子解释为什么 `var` 会得到 `3 3 3`,而 `let` 会得到 `0 1 2`,并说明函数作用域、块级作用域和 IIFE 在这里分别扮演什么角色。

·更新于 ·阅读约 9 分钟·计算中...
for 循环里 var 和 let 为什么结果不同

for 循环里 varlet 的经典区别,本质不是“一个旧一个新”,而是它们对每次迭代变量的作用域处理不同。你看到 3 3 30 1 2,其实是在观察闭包到底记住了“同一个变量”,还是“每轮一个新变量”。

目录

先看经典例子

function createCounters() {
  const counters = []

  for (var i = 0; i < 3; i++) {
    counters.push(function () {
      return i
    })
  }

  return counters
}

调用:

const counters = createCounters()

console.log(counters[0]())
console.log(counters[1]())
console.log(counters[2]())

结果是:

3
3
3

为什么是 3 3 3

因为闭包记住的不是“当时的数值副本”,而是变量 i 本身。var 是函数作用域,在整个 for 循环里只有一个共享的 i

循环跑完后:

i === 3

所以三个函数最后读到的都是同一个最终结果。

换成 let 为什么就变了

function createCounters() {
  const counters = []

  for (let i = 0; i < 3; i++) {
    counters.push(function () {
      return i
    })
  }

  return counters
}

现在输出会变成:

0
1
2

原因是 let 是块级作用域,循环的每一轮都会得到一个新的 i 绑定。闭包拿到的是每一轮自己的那个 i,而不是全局共享的一份。

真正要记住的不是“let 更高级”

而是:

  • var:整个函数里共享一个变量
  • let:每轮迭代都可能生成新的绑定

所以问题不在 for 本身,而在“闭包捕获的到底是哪一个变量绑定”。

IIFE 以前为什么能解决

let 普及之前,常见写法是 IIFE:

function createCounters() {
  const counters = []

  for (var i = 0; i < 3; i++) {
    ;(function (j) {
      counters.push(function () {
        return j
      })
    })(i)
  }

  return counters
}

这里的关键不是 IIFE 本身神奇,而是它人为创建了一个新的函数作用域,并把当前 i 作为参数值传给了 j。于是每次闭包都抓到了不同的 j

在现代代码里,如果没有兼容性包袱,直接用 let 更清楚。

什么时候这个坑最容易出现

  • for 循环里创建回调
  • setTimeout
  • 事件绑定
  • 异步任务队列

例如:

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

依然会打印三次 3

和闭包的关系到底是什么

闭包不是 bug,闭包只是“函数记住外部变量”的能力。真正让结果不同的是:

  • 你记住的是一个共享变量
  • 还是每轮独立的变量绑定

这也是为什么闭包、作用域和循环变量这三个概念总会在一起出现。

结论

  • varfor 里通常会让多个闭包共享同一个变量。
  • let 会为每轮迭代创建新的绑定,所以更符合直觉。
  • IIFE 是旧时代为了解决 var 共享变量问题的常见手段。

如果你接下来还在处理循环、遍历和数组回调的边界,可以接着看 JavaScript 数组与遍历指南;如果你在处理对象空值链路,继续看 可选链指南

7 分钟

JavaScript 中 var 和 let 的作用域差异解析

作用域定义了变量和函数的可访问范围。函数作用域:变量在整个函数内都有效,通常由var创建。块级作用域:变量只在其所在的代码块内有效,通常由let和const创建。在 JavaScript 中,var和letvar声明的变量具有函数作用域,这意味着它在整个函数内都有效。由于var的

8 分钟

TypeScript 里 as 和 satisfies 怎么选

解释 TypeScript 中 `as`、尖括号断言和 `satisfies` 的差异,重点说明为什么 `satisfies` 更适合对象结构校验,而 `as` 更像是在告诉编译器“先按我说的算”。

3 分钟

console.dir 和 console.log 有什么区别

说明 console.log 与 console.dir 在浏览器 DevTools 里的实际差异,重点回答对象调试、DOM 节点查看和 Node.js 环境下什么时候该用哪一个。

订阅 FreeMac

每周精选:免费 Mac 软件评测、可信来源更新、替代方案和少折腾指南。