了解 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(j);
}
/*
作用域如下:
A: {
j 在A作用域中
// 10个副本
}
*/
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。