JavaScript 闭包经典问题:为什么输出 10 次 i=10
JavaScript 闭包经典问题:为什么输出 10 次 i=10
问题代码
先观察以下代码,思考输出结果:
function f() {
for(var i = 0; i < 10; i++) {
setTimeout(() => {
console.log('i=', i)
});
}
}
f();
输出结果:
i= 10
i= 10
i= 10
...(共 10 次)
执行过程详解
第一步:var 变量的作用域
for(var i = 0; i < 10; i())
↑
└── var 声明的变量是函数作用域
整个函数 f 内都能访问这个 i
第二步:循环执行过程
循环次数 i 的值 循环条件 (i < 10)
---------------------------------------
第 1 次 0 ✓ 通过
第 2 次 1 ✓ 通过
...
第 10 次 9 ✓ 通过
10 ✗ 不通过,循环结束
循环结束后:i = 10
第三步:创建 10 个回调函数
for(var i = 0; i < 10; i++) {
setTimeout(() => {
console.log('i=', i) // ← 所有回调共享同一个 i
});
}
每次循环创建一个箭头函数,都通过闭包引用变量 i
第四步:异步执行时序
时间轴:
─────────────────────────────────────────
| 同步执行阶段 | 异步执行阶段 |
─────────────────────────────────────────
for 循环完成 setTimeout 回调执行
i 递增到 10 读取 i 的值(此时 i=10)
输出 10 次 i=10
─────────────────────────────────────────
核心原因
三个关键点
-
var 是函数作用域
- 不是块级作用域
- 整个函数内只有一个 i 变量
-
闭包共享变量
- 10 个箭头函数都引用同一个 i
- 不是创建 10 个独立的 i 副本
-
setTimeout 异步执行
- 回调函数放入任务队列延迟执行
- 执行时循环已结束,i 已经是 10
图示理解
变量 i 的生命周期:
─────────────────────────────→ 时间
0 1 2 3 4 5 6 7 8 9 10
└───┬───┘ └───┬───┘
│ │
同步循环执行 循环结束
i=10
回调函数 1: ────────────────────→ 读取 i (10)
回调函数 2: ────────────────────→ 读取 i (10)
...
回调函数 10: ────────────────────→ 读取 i (10)
解决方案
方案 1:使用 let(推荐)✨
function f() {
for(let i = 0; i < 10; i++) {
setTimeout(() => {
console.log('i=', i)
});
}
}
原理: let 是块级作用域,每次循环创建新的 i 绑定
输出: i= 0 到 i= 9 各一次
方案 2:IIFE 立即执行函数
function f() {
for(var i = 0; i < 10; i++) {
(function(j) {
setTimeout(() => {
console.log('i=', j)
});
})(i);
}
}
原理: 通过函数参数保存每次循环的 i 值
方案 3:传递参数给 setTimeout
function f() {
for(var i = 0; i < 10; i++) {
setTimeout((j) => {
console.log('i=', j)
}, 0, i);
}
}
原理: setTimeout 的第三个参数会传递给回调函数
知识点总结
| 概念 | 说明 |
|---|---|
| var 作用域 | 函数作用域,非块级作用域 |
| let 作用域 | 块级作用域,每次循环创建新绑定 |
| 闭包 | 函数可以访问其声明时所在作用域的变量 |
| 异步 | setTimeout 的回调会延迟执行 |
| 共享引用 | 同一作用域的闭包引用同一个变量 |
一句话总结
var 的函数作用域 + 闭包共享变量 + setTimeout 异步执行 = 所有回调读取到循环结束后的同一个 i 值(10)