let和var的区别也是一个老生常谈的问题,常见的回答就是
- let声明的变量存在块级作用域
- 同一作用域中,let声明的变量不允许重复声明
- let声明的变量不存在变量提升
- let存在"暂时性死区"
- let声明的变量会绑定当前这个作用域,即使父级作用域存在同名变量也会被屏蔽
前两点争议性不大,最后三点我认为都可以证明let声明的变量是存在变量提升的。首先明确一个概念,什么是"变量提升"?我理解的变量提升就是在函数的执行上下文压入上下文执行栈之后,在创建活动对象的时候,活动对象中有这个变量就是存在变量提升。
当一个函数的执行上下文被压入上下文执行栈中的时候,会首先执行如下几步操作: 假定这个函数的代码如下:
function fun(num){
let a = 1;
var b = 2;
function fun1(){
console.log(1);
}
}
- 复制函数的[[scope]]属性来创建函数执行上下文中的作用域链,这时的作用域链仍然不是完整的。
- 创建活动对象,这时候的活动对象包括三个部分:
- 根据传入函数的参数,创建arguments对象,假设传入函数的参数是一个数字1:
arguments: {
0: 1,
length: 1
}
- 函数声明,由函数名和对应的值组成的键值对
fun1:function(){
console.log(1);
}
- 变量声明。
a:'未初始化',
b:undefined,
var声明的变量此时是变量名和undefined组成的键值对,但是let声明的变量此时并没有初始值,如果在没初始值的情况下访问变量a就会出现报错信息,cannot access 'a' before initialization,也就是变量a没有初始化。这里也能看出,为什么在var声明变量之前访问的变量是undifined,但是在函数声明之前可以正常调用函数。
这时,变量对象就创建完成,此时的完整变量对象如下。
AO={
arguments: {
0: 1,
length: 1
},
fun1:function(){
console.log(1);
},
a:'未初始化',
b:undefined,
}
- 将活动对象压入子函数执行上下文中的作用域链顶端(unshift),这时候执行上下文中的作用域才是完整 的作用域链,之前的scope是不包括函数内部定义的变量的。
- 第四步:在代码执行阶段,会顺序执行代码,根据代码,修改活动对象的值。
接下来我就用这个假设一条条解释文章最前面说明的let的几个特性,再讨论下为什么这个假设是比较合理的。
- let声明的变量不存在变量提升
- let存在"暂时性死区"
- let声明的变量会绑定当前这个作用域,即使父级作用域存在同名变量也会被屏蔽
首先第一条:let声明的变量不存在变量提升,我认为let声明的变量是存在变量提升的,只是在创建活动对象的时候不像var声明的变量一样具有初始值undefined,let声明的变量的初始值是代码顺序执行到let这一行的时候才会给变量初始化。
console.log(a)//Error: Cannot access 'a' before initialization
let a;
console.log(a)//undefined
然后是第二条:let存在"暂时性死区","暂时性死区"是一个现象,我们需要讨论的是为什么会出现这个现象,使用这篇文章的假设就能很好的解释"暂时性死区"以及接下来需要讨论的变量绑定当前作用域,这也是为什么前面要一步步解释创建作用域链的过程。
之所以出现"暂时性死区"是因为此时变量未初始化,上面在声明变量a之前打印变量a会报错,显示变量a未被初始化。但是在执行let a;之后,变量a就有初始值undefined了。
最后是第三条:let声明的变量会绑定当前这个作用域,即使父级作用域存在同名变量也会被屏蔽。这是因为当前作用域的活动对象会被unshift进函数的属性[[scope]]中来创建执行上下文中的作用域链,所以在查找变量的时候,会先在当前函数作用域中的活动对象中找到let声明的变量,而不会继续沿着作用域链去其它活动对象中去寻找,我理解的作用域链就是活动对象的一个层级链。所以let声明的变量就像绑定了当前这个作用域,第二条和第三条都是一个现象,这两个现象的原因就是和活动对象的创建以及作用域链等有关。
在es6的文档中也是可以找到var/let hoisting的字样的,之所以很多博客会说let不存在变量提升,也是从表现上来解读的。这种解读方式对于初学者来说很友好,但是深入探讨let内部原理的时候需要更加深入了解内部的执行机制。以上的内容只是我的猜想,并没有找到证明的资料,如果有对这方面感兴趣的同学,欢迎评论区交流。