V8引擎里闭包变量为啥有时会意外共享?

ლ庆芳 阅读 14

我在写一个循环绑定事件的函数,发现每次点击都输出最后一个索引值,明明用了let声明变量啊。是不是V8对闭包的处理有什么特别的地方?

试过改成用const、也试过把回调抽成单独函数,但问题还是存在。代码大概是这样的:

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

按理说应该输出0、1、2,但实际运行确实如此,可在我项目里类似的结构却总是输出3,怀疑是不是V8优化导致变量被复用了?

我来解答 赞 4 收藏
二维码
手机扫码查看
1 条解答
W″恩宇
JS里面这个现象其实跟V8的闭包实现关系不大,更多是作用域和执行时机的问题。

你给的这个例子:
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 100);
}
按理说确实会输出0、1、2,因为let在for循环里是块级作用域,每次迭代都会创建一个新的i绑定,V8底层会用“循环变量快照”的方式来处理,每个闭包捕获的是各自迭代时的那份i,不是共享同一个变量。

但你项目里输出3,大概率是这种结构出了问题——比如你可能是在循环外部声明了回调函数,或者把回调抽成函数时没传入i,或者用var混着用,或者循环里还有异步嵌套、或者用了async函数+await之类的结构,导致闭包捕获的是同一个变量引用。

举个容易踩的坑:
for (let i = 0; i < 3; i++) {
const fn = () => console.log(i);
setTimeout(fn, 100);
}

这个其实也会输出0、1、2,没问题。
但如果像这样:
let i;
for (i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}

那就会输出3、3、3——因为i是在循环外部声明的,所有闭包都指向同一个变量,等setTimeout执行时循环早结束了,i已经是3。

还有一种情况:你可能用的是var,或者在某些老版本引擎里let配合try/catch有兼容问题(虽然V8早就修了),但更可能是你抽函数的时候漏了传参,比如:
function log() {
console.log(i);
}
for (let i = 0; i < 3; i++) {
setTimeout(log, 100);
}

这个log里的i是外层作用域的,不是每次循环的那份,所以肯定输出3。

你把项目里那段代码贴出来看看,我一眼就能定位问题。V8闭包这块很稳,真出问题基本都是人写的问题。
点赞 2
2026-02-24 17:08