为什么var与let在闭包中输出不同?循环输出差异疑问
这个问题太典型了!刚上手ES6的时候我也被这个坑绊过,咱们先把代码摆出来,再一步步说清楚原因。
首先看用var的版本:
for (var i = 0; i < 10; i++) { setTimeout(() => { console.log(i); }, 100); }
运行这段代码,你会看到控制台连续打印10次10。
再看用let的版本:
for (let i = 0; i < 10; i++) { setTimeout(() => { console.log(i); }, 100); }
这次会依次打印0到9,和你预期的一样。
核心原因:作用域 + 变量绑定方式的差异
先聊var的问题
var是函数作用域的变量,它的作用域范围是整个函数(如果不在函数里就是全局作用域),而不是for循环的{}块。也就是说,整个循环从头到尾只有一个i变量。
循环会先快速跑完(因为setTimeout是异步的,会被放到任务队列里,等主线程空闲才执行),跑完的时候i已经变成了10(因为最后一次循环i=9,执行i++后变成10,不满足i<10的条件,循环终止)。
等100ms后,所有setTimeout的回调开始执行,它们都引用的是同一个i变量——这个时候i的值已经是10了,所以就会打印10次10。这里其实还涉及到闭包:每个回调函数都捕获了外部作用域中i的引用,而不是捕获当时的数值。
再看let的解决之道
let是块级作用域的变量,它的作用域被限制在当前的{}块里。更关键的是:在for循环中使用let时,每次迭代都会创建一个新的i变量绑定——相当于每次循环都重新声明了一个i,并把当前循环的数值赋值给它。
所以每个setTimeout的回调函数,捕获的都是当前迭代那个独立的i变量。等异步回调执行时,每个i都保留了当时迭代的数值(0到9),自然就会依次打印出正确的结果。
用var模拟let的效果
如果你想用var实现和let一样的输出,可以用立即执行函数(IIFE)手动创建块级作用域,把每次循环的i值“保存”下来:
for (var i = 0; i < 10; i++) { // 用IIFE创建独立作用域,把当前i的值传给j (function(j) { setTimeout(() => { console.log(j); }, 100); })(i); }
这段代码也会打印0到9,本质就是手动做了let自动帮你做的事情——给每次迭代创建独立的变量绑定。
总结一下:var和let的差异不只是表面的作用域范围,在for循环这种场景下,let的“每次迭代创建新绑定”特性才是导致输出不同的关键,而var因为共享同一个变量,异步回调只能拿到最终的数值。
内容的提问来源于stack exchange,提问作者Ayush Gupta

