核心代码
let i = 0
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
结果:6 6 6 6 6 6
for(let i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
结果:0 1 2 3 4 5
1.结果为什么是6个6
核心:函数的结果与其执行环境与时机、作用域等因素都有直接决定关系
执行环境
核心问题:JS语言的执行环境是单线程的
JS的执行环境是单线程的。代码会从上到下依次执行(同步执行);for循环是同步代码,setTimeout是异步代码。JS在执行代码的过程中,碰到同步代码会依次执行,碰到异步代码就会将其放入任务队列中进行等待,当同步代码执行完毕后再开始执行异步代码(异步执行)。
- 同步执行:按照代码顺序执行,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的
- 异步执行:每一个任务有一个或多个回调函数(callback),异步代码任务执行完成后会执行回调函数,后续的任务则是不需要等待之前的任务结束即可执行,程序的执行顺序与任务的排列顺序不一定一致
执行时机
同步:
let a = 1
function fn(){
console.log(a)
}
fn() //1
a=2
console.log(a) // 2
所有的代码按照预定的顺序执行
- 声明变量 a,变量的值为1
- 声明函数 fn,该函数的作用是在控制台输出 a 的值
- 调用函数 fn(),输出a的值为1
- a 赋值为 2
- 输出a的值为 2
在这段代码中所有的函数按照其编写的顺序执行
异步:
let i = 0
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
setTimeout是异步函数,会根据回调延后执行,整体流程如下:
- 声明变量 i
- i 赋值为 0
- 判断i<6,满足条件
- 进入循环,执行setTimeout
- setTimeout 根据回调函数延后输出
- i自增,重复步骤3、4、5、6,当i=6 时,不满足条件循环结束
- setTimeout 开始执行, 输出 i 的值6,并重复6次
即使时间延迟设为0也不改变其作为异步执行代码的本质。JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列负责外一些代码的执行。
作用域
-
当同步代码执行完毕后,开始执行异步的setTimeout代码,执行setTimeout时需要从当前作用域内寻找一个变量i,此时for循环已执行完毕,当前 i=6,当let在for循环的外部时,其作用域是循环外的,i的值变化的所有执行结果都有效。
-
所以执行setTimeout时输出为6,任务队列中的剩余5个setTimeout也依次执行,输出为6。
-
将let声明移动到循环内部后,let只在代码块内才有效,i只在本轮循环中有效,每次循环的i 其实都是一个新的变量,所以setTimeout定时器里面的i,其实是不同的变量,即最后输出0-5。(每次循环的变量i都是重新声明的)
2. 其他方法
核心思想:通过在闭包形成新的作用域,从而在每次迭代中,捕获这次迭代中对应的i
- 直接使用闭包配合其他变量存储对应的值
let i = 0
for (i = 0; i < 6 ; i++) {
(function() {
var j = i // 在闭包作用域中,通过添加自己的变量,每次迭代都捕获i的副本
setTimeout( function timer() {
console.log(j);
}, 1000)
})()
}
- 也可以直接在返回值中进行捕获
let i=0
for (i = 1; i <= 5; i++) {
setTimeout(function (i) {
return function timer(){
console.log(i);
}
}(i), 1000)
}
- 直接执行函数形成的闭包更直观
function timer() {
let i = 0
for (i = 0; i < 6; ++i) {
(function(i){
setTimeout(function() {
console.log(i);
}, 1000)
})(i)
}
}
timer()