[JavaScript]for循环的初始化块中的词法声明以及作用域
这段代码,你觉得会输出什么?
for (
let i = 0, getI = () => i, incrementI = () => i++;
getI() < 3;
incrementI()
) {
console.log(i);
}
别偷看答案,先想一下,是 0 1 2
,还是 3 3 3
,还是 0 0 0
?
.
.
.
.
.
. 密
. 封
. 线
.
.
.
.
.
答案是 0 0 0
。为什么?这就要明白 for 循环的初始化块中的词法声明中,变量的作用域是什么。
for 循环的结构是这样的:
for (initialization; condition; afterthought)
statement
其中的 initialization
叫做初始化块,通常用于声明计数器变量。condition
是每次循环之前要判定的条件的表达式。afterthought
是每次循环结束时执行的表达式。前面这些都是可选的。statement
是每次 condition
判断为真时要执行的语句。
对于初始化块的作用域,MDN 上这样解释:
初始化块的作用域范围可以理解为声明发生在循环体内部,但实际上只能在 condition 和 afterthought 部分中访问。更准确地说,let 声明是 for 循环特有的——如果 initialization 是 let 声明,那么每次循环体执行完毕后,都会发生以下事情:
使用 let 声明新的变量会创建一个新的词法作用域。
上次迭代的绑定值用于重新初始化新变量。
afterthought 在新的作用域中执行。新的词法作用域会在 initialization 之后、condition 第一次被判定之前创建。initialization 内部的变量与每次迭代中的变量是不同的,包括第一次。
根据这个解释,文章开头的代码的执行流程是这样的:
- 在初始化块中声明了一个变量
i
,它的值是1
;声明了一个函数getI
,它读取并返回i
;声明了一个函数incrementI
,它首先读取并返回i
,之后将i
增加1
。注意!这两个函数和变量i
处于同一个作用域下,并且它们都引用了i
,在后续步骤中,这两个函数处于不同的作用域下,依然能够读取这里的i
,这就是闭包。 - 在第一次循环时,创建了一个新的作用域,这个时候的变量
i
,并不是步骤 1 中的变量i
!它们不在同一个作用域中。根据文档描述,它的值应该使用上次循环的值(文档中对此值的称呼为“上次迭代的绑定值”,你可以理解为i
自增后的那个值),但现在是第一次循环,它的值应该取的是初始值,因此是0
(这在文档中没有描述,如果你知道哪里有确切的描述,欢迎在评论区指出)。在第一次的条件判断中,函数getI
实际上读取的是步骤 1 中的i
,返回0
。在statement
中,打印了当前的变量i
的值,输出0
。最后,在 afterthought 部分,调用了函数incrementI
,而函数incrementI
也是闭包,它读取的依然是步骤 1 中的变量i
的值,因此,步骤 1 中的变量i
此时是1
,而当前作用域中的变量i
的值依然为0
,因为并没有什么地方改变它的值。 - 第二次循环,又创建了一个新的作用域,在此作用域中创建了新的变量
i
,它的值是上次循环的值0
。其他流程和步骤 2 相同。此时步骤 1 中的i
的值为2
,而本次循环中的i
依然是0
。继续输出一个0
。 - 重复步骤 3,步骤 1 中的
i
的值增加为3
,依然输出一个0
。 - 不符合判断条件,循环退出。
从这段代码可以看出 for 循环的初始化块是如何创建作用域并进行词法声明的。通过闭包绑定初始的 i,可以做到不影响 statement 部分的 i 的值。