相信大家在日常开发中经常用到for循环,比如
for (var i = 0; i < 1; i++)
for (let i = 0; i < 1; i++)
上述两个for循环语句,差别就是声明i的时候一个使用var声明,一个使用let声明,相信大家都知道let和var的区别,这里就不再赘述,可你们知道这里的深层原理是什么吗,是什么导致了他们之间的区别呢,他们的区别又会造成什么样的影响呢?

语句块级作用域

在JavaScript中,只有4种语句拥有块级作用域:

// try catch 中每一个大括号都是一个作用域
try {
} catch(e) {
} finally {
}

with(x) /* 这里有个作用域 */

{
  // 这里是个作用域
}

上面举例的三种,try-catch-finally语句中看似也是利用了{}划分的作用域,但是因为try-catch-finally每一部分都不能允许出现单语句,所以它是独立的一类,with语句后面不加{}是一个作用域(即使它是单语句),也可以利用{}包裹起来形成块。
每一个语句块都有返回值,可以用eval查看他们的返回值,例如

console.log(eval(`
try {
  1 + 1
} finally {
  2 + 2
}
`));

其他像if语句是没有块作用域的,if后面可以接单语句也可以接{}{}会形成一个作用域。
如果像if语句是有块作用域的话,那么以下代码是可以成立的

if (true) let a = 1;

如果if会形成一个作用域,那么我在作用域内是可以声明一个变量的,实际上这么做的话会报Lexical declaration cannot appear in a single-statement context

刚才讲了3种,还有一种呢?
还有一种就是for(let/const)循环语句。

for循环包含了for语句和循环体,循环体可以是单语句也可以是{}块。
因为历史原因,之前js只有global scope 和 function scope,var声明的变量会登记在最近的上述作用域中。而let/const声明是放在词法作用域,所以声明方式不同导致了for(var)没必要形成一个单独的作用域。

for循环中的隐蔽之处

我先说一下for(let/const)循环作用域的结构

for (let i = 0; i < 10; i++) // 作用域A
{
    // 作用域B
    console.log(i);
}
/*
    作用域如下:
    A: {
        B: {
            // 10个副本
        }
    }
 */

what?有这么多作用域,而且作用域B还有10个副本?
先看看为什么会有两个作用域的分割(A和B)。
先看看下面的代码

for (let i in obj) {
    console.log(i);
}

假设for循环只需要一个作用域,那么在每一次遍历的时候都重新进行i的声明,造成遍历重复声明的后果,所以这里实际上是这样的:外层有一个forEnv,作声明i,里面有一个loopEnv,作遍历,这样声明只有一次,并且遍历过程也能访问到i。

那刚才的例子为什么B作用域有10个副本呢?
相信大家都知道for循环中,每一次循环的变量在当前次循环能访问到正确的值

for (let i = 0; i < 10; i++) {
    setTimeout(() => console.log(i), i * 10);
}

看到这个例子相信大家都知道我在讲什么了,现象是每次循环中都有一个定时任务,而这个定时任务触发的时候能正确地访问到当前次循环的i值。如果i只有一个,那么循环结束后i的值应该是10,所有的定时回调应该都是打印10才对。所以for(let/const)循环中,每一次循环都是作用域的副本,所以for(let/const)循环可能并不比执行10次函数的开销小。

参考

《JavaScript核心原理解析》周爱民


一画先生
83 声望12 粉丝

我司长期招聘前端开发工程师,有意的小伙伴+vx: Mr_yihua