var

1
2
3
4
5
for(var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
})
}
  • var 声明提升
    1. 使用 var 关键字声明的变量会自动提升到函数作用域顶部,为局部变量
    2. var 声明不是包含在函数内,则提升到 script 标签下方,成为全局变量
  • for循环有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域

所以原 for 循环可以分解为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var i = undefined   // 全局变量     

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


// i == 1
i < 3
{
setTimeout(() => {
console.log(i);
});
}
i++


// i == 2
i < 3
{
setTimeout(() => {
console.log(i);
});
}
i++ // i == 3
  • setTimeout 是异步执行,每一次 for 循环的时候,setTimeout 都执行一次,但是里面的 回调函数 没有被执行,而是被放到了任务队列里,等待执行。只有主线上的任务执行完,才会执行任务队列里的任务
  • setTimeout 延时参数:没写,则默认延时 0;这个延迟时间始终是相对主线执行完毕的那个时间算的 ,并且多个 setTimeout 执行的先后顺序也是由这个延迟时间决定的

进一步解析,简化结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 主线
var i = undefined

i = 0
i = 1
i = 2
i = 3

// 任务队列,延时都为 0
{
//setTimeout(
() => {
console.log(i);
}
//);
}

{
//setTimeout(
() => {
console.log(i);
}
//);
}

{
//setTimeout(
() => {
console.log(i);
}
//);
}

从上往下执行,setTimeout回调函数:延时都为 0,同时执行;执行时找不到变量 i,到上一级作用域找,找到全局变量 i,值为 3

let

1
2
3
4
5
for(let j = 0; j < 3; j++) {
setTimeout(function() {
console.log('j', j);
})
}
  • let 声明的范围是 块作用域,即距离 let 最近的外层 {}
  • 个人理解:由于 var 命令的变量提升机制,var 命令实际只会执行一次。而 let 命令不存在变量提升,所以每次循环都会执行一次,声明一个新变量(但初始化的值不一样),JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量 i 时,就在上一轮循环的基础上进行计算。for 的每次循环都是不同的块级作用域,let 声明的变量是块级作用域的,所以也不存在重复声明的问题
  • 《JavaScript高级程序设计4》:使用 let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量,每个 setTimeout 引用的都是不同的变量实例

可以分解为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
// 块作用域 1
{
let j = 0
j < 3
{
setTimeout(function() {
console.log(j);
}
j++
}

// 块作用域 2
{
let j = 1
j < 3
{
setTimeout(function() {
console.log(j);
}
j++
}

// 块作用域 3
{
let j = 2
j < 3
{
setTimeout(function() {
console.log(j);
}
j++
}

// 块作用域 4
{
let j = 3
}
}

《JavaScript高级程序设计4》:JavaScript 引擎会记录用于变量声明的标识符及其所在的块作用域。同样的:setTimeout 是异步执行,被放到了任务队列里,等其余代码执行完毕,再执行里面的函数。setTimeout 执行时找不到变量 j,到上一级块作用域(let声明的作用域)找,找到变量 j,执行结果:j 0j 1j 2

总结

  1. 此处 var 定义的 i 是贯穿整个作用域的,而不是给每一个定时器分配一个 i ,定时器是异步,一定是在 for 运行完了以后,定时器内的回调函数才开始执行,此时 i 已经变成 3 了
  2. 此处 let 定义的 j 具有块级作用域,给每一个定时器都分配了一个 j,for 运行完了执行回调的时候,也就找到了对应的 j
  3. setTimeout 换为其他的异步操作,也同理(如点击事件的回调,当触发点击的时候,for 肯定已经运行完了)