这段代码,你觉得会输出什么?

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 内部的变量与每次迭代中的变量是不同的,包括第一次

根据这个解释,文章开头的代码的执行流程是这样的:

  1. 在初始化块中声明了一个变量 i,它的值是 1;声明了一个函数 getI,它读取并返回 i;声明了一个函数 incrementI,它首先读取并返回 i,之后将 i 增加 1。注意!这两个函数和变量 i 处于同一个作用域下,并且它们都引用了 i,在后续步骤中,这两个函数处于不同的作用域下,依然能够读取这里的 i,这就是闭包
  2. 在第一次循环时,创建了一个新的作用域,这个时候的变量 i,并不是步骤 1 中的变量 i!它们不在同一个作用域中。根据文档描述,它的值应该使用上次循环的值(文档中对此值的称呼为“上次迭代的绑定值”,你可以理解为 i 自增后的那个值),但现在是第一次循环,它的值应该取的是初始值,因此是 0(这在文档中没有描述,如果你知道哪里有确切的描述,欢迎在评论区指出)。在第一次的条件判断中,函数 getI 实际上读取的是步骤 1 中的 i,返回 0。在 statement 中,打印了当前的变量 i 的值,输出 0。最后,在 afterthought 部分,调用了函数 incrementI,而函数 incrementI 也是闭包,它读取的依然是步骤 1 中的变量 i 的值,因此,步骤 1 中的变量 i 此时是 1,而当前作用域中的变量 i 的值依然为 0,因为并没有什么地方改变它的值。
  3. 第二次循环,又创建了一个新的作用域,在此作用域中创建了新的变量 i,它的值是上次循环的值 0。其他流程和步骤 2 相同。此时步骤 1 中的 i 的值为 2,而本次循环中的 i 依然是 0。继续输出一个 0
  4. 重复步骤 3,步骤 1 中的 i 的值增加为 3,依然输出一个 0
  5. 不符合判断条件,循环退出。

从这段代码可以看出 for 循环的初始化块是如何创建作用域并进行词法声明的。通过闭包绑定初始的 i,可以做到不影响 statement 部分的 i 的值。

标签: JavaScript, 闭包, for

添加新评论