乐趣区

关于javascript:For循环中的let和var

理解 let 的个性是从 MDN 的文档,我失去的信息有这么几条:

  • let 申明的变量的作用域是块级的;
  • let 不能反复申明已存在的变量;
  • let 有临时死区,不会被晋升。

大部分人应该都是这么认为的,这个了解「没有问题」,然而不够「全面和粗浅」。

质疑:
for (var i = 0; i<=5; i++){setTimeout(()=>{console.log(i)})
}

大家都晓得会打印出 6 个 6。如果把 var 改成 let,就会别离打印出 0、1、2、3、4、5:

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

然而,用后面的常识并不能很好的从原理上来解释这个起因。

于是我去看 MDN 的例子

发现鸡贼的 MDN 奇妙地避开了这个问题,它的例子是这样的:

为什么 MDN 要成心申明一个 j 呢,为什么不间接用 i 呢?我猜想 MDN 为了简化常识,瞒哄了什么。
于是我去看了看 ES6 原英文文档。

总结:
一、当 for 循环中应用 let 关键字的时候,在这个 () 块中会生成一个作用域,咱们称之为词法作用域或者块级作用域。ES5 之前 JavaScript 只有 global scope 和 function scope,var 申明的变量会注销在最近的上述作用域中。例如:for( let i = 0; i< 5; i++) 这句话的圆括号之间,有一个暗藏的作用域
for (let i = 0; i < 10; i++) // 作用域 A
{
    // 作用域 B
    console.log(i);
}
/*
    作用域如下:A: {
        B: {
            i 变量在 B 作用域中
            // 10 个正本
        }
    }
 */
二、在 for 循环中变量 i 是由 var 申明的所以变量 i 会被晋升至 for 循环外的作用域顶部,所以即使是在循环之外也能够拜访变量 i。例如:
for (var i = 0; i < 10; i++) {process(items[i]);
}
console.log(i)  // 在这里任然能够拜访变量 i
console.log(i)  // 但如果换成 let 在这里不能够拜访变量 i,抛出谬误
三、for(let i = 0; i< 5; i++) {循环体} 在每次执行循环体之前,JS 引擎会把 i 在循环体的上下文中从新申明及初始化一次。联合一代码块来看,10 个正本 B 作用域中都会从新申明及初始化一次 i,那么如果 B 作用域中如果是一个函数setTimeout(()=>{console.log(i)}),产生了闭包。那么闭包中的 i 自然而然仍放弃着对外部 B 作用域中流动对象 i 的援用。后果如下:

四、顺着来解释为什么 for (var i = 0; i<=5; i++){setTimeout(()=>{console.log(i)})} 输入是 6 个 6。如下图在每次执行循环体之前,i 并不会再循环体上下文中从新申明和初始化一次,这个步骤在 i 理论所在内部就近函数或者全局函数中实现。因而,当 A 作用域中如果是一个函数setTimeout(()=>{console.log(i)}),产生了闭包。按照闭包中 i 仍放弃着对外部 A 作用域中流动对象 i 的援用的逻辑,然而作用域 A 中并没用 i。所以顺着作用域链往上找,直到找到理论 i 所在内部就近函数或者全局函数,找到了还须要等循环走完,返回理论 i 为 6 的值。
for (var i = 0; i < 10; i++)
{
    // 作用域 A
    console.log(i);
}
/*
    作用域如下:i 在内部就近函数或者全局函数中
    A: {// 10 个正本}
 */
五、咱们用一个代码块来补充四中的解释。下图咱们能够看到如果咱们把 i 赋值给 j 并放入 A 作用域中的话,如果外部还是闭包,那么根据闭包准则,仍旧能实现对 A 作用域中变量 j 的援用,所以能失去咱们想要的后果。
for (var i = 0; i < 10; i++)
{
    let j = i
    // 作用域 A
    console.log(i);
}
/*
    作用域如下:A: {
        j 在 A 作用域中
        // 10 个正本
     }
 */

退出移动版