JS循环与闭包疑问:为何setTimeout输出10而非9?
嘿,这几个问题我刚入门JS的时候都踩过坑,太有共鸣了!咱们一个个来,优先把最核心的第一个问题掰透~
问题1:为什么for (var i=0; i < 10; i++){ setTimeout(function(){ console.log(i); }, 1000); }会十次输出10?
这事儿核心要抓两点:var的作用域特性和**setTimeout的异步执行逻辑**。
首先,var声明的变量是「函数级作用域」(如果这段代码不在函数里就是全局作用域),整个循环从头到尾只有同一个i变量,不是每次循环都新建一个i。
然后,setTimeout是异步任务——它不会立刻执行里面的回调函数,而是把回调扔进浏览器的任务队列,等当前的同步代码(也就是整个for循环)跑完之后,才会依次执行这些回调。
那循环的执行过程是啥样的?
- 一开始i=0,满足
i<10,执行一次setTimeout(把回调存起来),然后i变成1; - 重复这个过程,直到i=9的时候,还是满足
i<10,执行第10次setTimeout,然后i变成10; - 这时候
i<10不成立,循环停止。
等1秒后,任务队列里的10个回调开始执行,它们要找的都是同一个i变量,而这时候i已经是10了——所以每个回调都会输出10,一共十次。
为啥不是九次9?因为循环一共执行了10次(i从0到9,每次对应一次setTimeout),所以有10个回调,自然输出十次10。
问题2:为什么同步的for(var i=0; i<10; i++){ console.log(i) }会依次输出0-9?
这个就简单了——因为这里的console.log是同步执行的,每次循环迭代里,console.log都会立刻拿到当前i的值:
- 第一次循环i=0,直接输出0;
- 第二次i=1,直接输出1;
- ...直到i=9,输出9,然后i变成10,循环停止。
和第一个问题的本质区别就是:同步代码按顺序立刻执行,异步代码要等同步代码跑完才执行,这时候i已经变了。
问题3:为什么for(let i=0; i<2; i++){ setTimeout(()=>console.log(i)) i++ }输出1而非2?
这里要抓let的「块级作用域」特性和循环的执行流程:
首先,let声明的变量是「块级作用域」,每次for循环迭代都会新建一个独立的i变量,每个回调绑定的都是当前迭代的那个i。
然后看这段代码的执行步骤:
- 初始化
let i=0,第一次判断0<2,满足条件进入循环体; - 执行
setTimeout(把回调存起来,绑定当前迭代的i); - 循环体里执行
i++,当前迭代的i变成1; - 执行for循环自带的后置
i++(这一步是为下一次迭代准备的,把下一个i变成2); - 第二次判断
2<2,不满足,循环直接停止——所以整个循环只跑了1次,只有1个回调; - 等当前同步代码跑完,回调执行,它绑定的是第一次迭代里被改成1的i,所以输出1。
为啥不是2?因为循环只执行了一次,而且回调绑定的是第一次迭代的i(最后被改成了1),下一次迭代根本没启动,自然不会输出2。
内容的提问来源于stack exchange,提问作者Robert O'Toole

