1

了解 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 巧妙地避开了这个问题,它的例子是这样的:
image.png
为什么 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的引用。结果如下:

image.png

四、顺着来解释为什么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(j);
}
/*
    作用域如下:
    A: {
        j 在A作用域中
        // 10个副本
     }
 */

image.png


Macrohoo
28 声望2 粉丝

half is wisdom!🤔