由 let 和 for 引起的血案

这是一道出自 HTTP 203 的 JS 题目。HTTP 203 是 Youtube 上的一个栏目,主要讲一些有趣的知识。

原题目是这样的:

1
2
3
4
5
6
7
8
9
for(
let i = (setTimeout(()=>console.log(i), 2333), 0);
i < 2;
i++
) {

}

// 问 2333 毫秒之后打印出什么

答案是 2333 毫秒后打印出 0。 为什么呢?

在开始分析题目之前,我们先来回顾几个知识点:

for 语法

1
2
3
for (语句 1; 语句 2; 语句 3) {
被执行的代码块
}

语句 1(代码块)开始前执行;
语句 2 定义运行循环(代码块)的条件;
语句 3 在循环(代码块)已被执行之后执行;

执行的顺序为:
1.第一次循环,即初始化循环。
首先执行语句1(一般为初始化语句),再执行语句2(一般为条件判断语句),判断语句1是否符合语句2的条件,如果符合,则执行代码块,否则,停止执行,最后执行语句3。
2.其他循环:
首先判断前一次语句3的执行结果是否符合执行语句2的条件,如果符合,继续执行代码块,否则停止执行,最后执行语句3。如此往复,直到前一次语句3的执行结果不满足符合执行语句2的条件。

总的来说,执行顺序是一致的,先执行条件判断(语句2),再执行代码块,最后执行语句3。如此往复,区别在于条件判断的对象,在第一次判断时,是执行语句1,初始化的对象,后续的判断对象是执行语句3的结果。

逗号表达式

逗号表达式,因为原题目中就有使用逗号表达式let i = (setTimeout(()=>console.log(i), 2333), 0);

逗号表达式的一般形式是:表达式1,表达式2,表达式3……表达式n。
逗号表达式的求解过程是:先计算表达式1的值,再计算表达式2的值,……一直计算到表达式n的值。最后整个逗号表达式的值是表达式n的值。 看下面几个例子:

1
2
3
4
5
6
7
x=8*2, x*4  // 整个表达式的值为64,x的值为16

(x=8*2, x*4), x*2 // 整个表达式的值为32,x的值为16

x=(z=5, 5*2) // 整个表达式为赋值表达式,它的值为10,z的值为5,x的值为10

x=z=5, 5*2 // 整个表达式为逗号表达式,它的值为10,x和z的值都为5

逗号表达式用的地方不太多,一般情况是在给循环变量赋初值时才用得到。所以程序中并不是所有的逗号都要看成逗号运算符,尤其是在函数调用时,各个参数是用逗号隔开的,这时逗号就不是逗号运算符。

基础知识回顾完毕,我们通过几个简单示例一步一步地逼近原题目:

示例一:基础知识 for 循环

1
2
3
4
5
for (var i = 0; i < 2; i++) {
console.log(i);
}

// 打印什么

这个无需多说,答案输出 0 1。

示例二:我们稍微改造下,将 log 放入 setTimeout 中

1
2
3
4
5
for (var i = 0; i < 2; i++) {
setTimeout(() => console.log(i));
}

// 打印什么

答案输出 2 2。分析下:
上述代码中,变量 i 是 var 命令声明的,在全局范围内都有效,所以全局只有一个变量 i。每一次循环,变量 i 的值都会发生改变,而循环内被赋给 setTimeout 内部的 console.log(i),里面的 i 指向的就是全局的 i。也就是说,这里面所有的 i 指向的都是同一个 i,导致运行时输出的是最后一轮的 i 的值,也就是 2。

示例三:我再稍微改造下,将上述 var 改为 let。

1
2
3
4
5
for (let i = 0; i < 2; i++) {
setTimeout(() => console.log(i));
}

// 打印什么

答案输出 0 1。分析下:
上述代码中,变量 i 是 let 声明的,当前的 i 只在本轮循环有效,所以每一次循环的 i 其实都是一个新的变量,所以最后输出的是0 1。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

原题目

1
2
3
4
5
6
7
8
9
for(
let i = (setTimeout(()=>console.log(i), 2333), 0); // 语句1
i < 2; // 语句2
i++ // 语句3
) {

}

// 问 2333 毫秒之后打印出什么

答案是 2333 毫秒后打印出 0。分析下:
上述题目中,变量 i 是 let 声明的,当前的 i 只在本轮循环有效,后面的表达式是逗号表达式,取最后一个值,即 i = 0,settimeout 在语句1,由于语句1只在第一次循环执行,因此 settimeout 的作用域是第一次迭代的作用域,且只执行一次。第一次迭代时 i = 0,所以答案是 2333 毫秒后打印出 0。

您的支持将鼓励我继续创作!