let、const、var是JS中的声明关键字
let和const
我们熟知的let和const的特性,常见的就有以下四点:
1.let和const声明的变量在未初始化之前不可以被使用。(暂时性死区TDZ)
2.let和const声明的变量,在同一个执行上下文中不可以被重复声明。
3.let可以只声明,后面再赋值,未赋值的话初始化值为undefined。
4.const一经声明必须马上初始化。
var
1.可以在声明之前访问 2.可以在同一个执行上下文中声明重复的变量。
但是如果问我,let和const声明的变量为什么会这样,为什么跟var声明的变量不一样,一时间我又说不出个所以然。所以就去研究了一下ECMA的文档。
Let and Const DeclarationsES6原文定义:
let和const声明的变量
会挂载到执行上下文的词法环境当中去,这些变量会在包含它们的环境记录项初始化的时候被创建。但是在变量的词法绑定执行之前他们是无法被访问的。通过词法绑定定义的变量,如果包含初始化语句,那么词法绑定执行的时候,赋值操作也会执行,而不是在变量创建的时候赋值。 let语句,允许变量在词法绑定的时候不同时做初始化操作,它会在词法绑定的时候初始化为undefined。
从这个定义当中,我们可以粗略提取出我们了解过的let和const的特性。 但是每个特性具体是怎么实现的,还要根据其声明的具体实现来看。
从let和const声明的静态语义语法来看,它分为两大阶段:
- LexicalDeclaration词法声明 错误检查阶段: 在词法声明阶段会检查let不能作为变量关键字,以及绑定列表中不允许出现重复的标识符。(这就是特性2) 标识符绑定阶段: 把标识符名称添加到绑定的标识符列表中(BindingList)
- LexicalBinding词法绑定 错误检查阶段: 词法绑定阶段会去判断const声明变量是否带有初始化。(特性4 get) 标识符初始化阶段: 执行声明的时候自带初始化器的标识符的初始化。(也可以理解为把执行赋值操作)没有初始化的let声明变量赋值为undefined(特性3 get) 所以我们可以看到let声明过程中,它的标识符变量创建和初始化与赋值是分开的
但是到目前为止,let和const暂时性死区这个特性其实理解起来还是没有那么直观。
为了有更清楚的对比,我们先看var变量声明的过程中发生了什么。
VariableStatement:
var声明
的变量会挂载到执行上下文的变量环境当中。当变量包含的环境记录项在创建的时候即会被实例化和初始化为undefined。 当var声明的变量,如果带有赋值语句,赋值操作会在代码执行的时候完成,而不会在变量一声明创建的时候就执行。
所以看完var和let的声明语义之后,我们可以总结出来: 变量声明其实分为三个步骤:
1.变量标识符的创建
2.变量标识符的初始化
3.变量标识符的赋值
var
的处理方式是,标识符创建的时候,不管你有没有赋值语句,它先把变量初始化为undefined。
如果var语句后面跟了赋值语句,在创建完标识符之后,代码执行阶段再把变量标识符的值更新成赋值的内容。
可以说var声明的变量,不管
例如var a = '123';
这个语句可以拆解成var a; a='123';
,在声明解析变量标识符绑定阶段,JS只去解析了var a
声明,因为var声明的特性,此时a在变量环境中被创建,并且直接初始化为undefined了。
但是只有当JS到了执行阶段,才会去执行a='123';此时变量环境中的a取值才会被更新为'123';
这就是为什么以下代码能执行,并且顺序不同打印的结果不同。
console.log(a);//undefined
var a = '123';
var b;
console.log(b);//undefined
console.log(a);//123
b = 2;
console.log(b);//2
这个的执行可以抽象成这样:
var a;//变量标识符声明创建阶段,此时a没赋值不能访问
var b;//变量标识符声明创建阶段,此时b没赋值不能访问
a = undefined;//变量标识符声明创建阶段,默认赋值可以访问了
b = undefined;
console.log(a);
a = '123';
console.log(b);
console.log(a);
b = 2;
console.log(b);
那么再说回到let,let声明的过程也是有创建、初始化、赋值三部曲。 但是问题是,它和var的区别就在于,let标识符创建的时候就只创建了变量,标识符名称绑定了,但是初始化是等到赋值阶段再初始化,也就是说如果let声明的变量带了赋值内容,就不在初始化了,没有才会初始化为undefined。 所以当执行的时候,let不能在它声明之前使用。
console.log(p);//Uncaught ReferenceError: p is not defined
let p;
{
console.log(x) // Uncaught ReferenceError: Cannot access 'x' before initialization
var c = 2;
let x = 1
}
console.log(c);//2
console.log(x);//Uncaught ReferenceError: x is not defined
上面这个例子当中,{}
块语句产生了新的块作用域。
let声明的变量是绑定到{}
块作用域里面的,所以在块作用域之外要访问x会报错,但是 var声明的变量是挂载到当前的函数作用域里面的,所以可以访问。
Advanced JavaScript ES6 — Temporal Dead Zone, Default Parameters And Let vs Var — Deep dive!
The Difference Between Function and Block Scope in JavaScript
JavaScript ReferenceError – Can’t access lexical declaration`variable’ before initialization
总结:变量提升和暂时性死区(TDZ)
- 变量提升:代码顺序上,变量调用在前,声明在后,可以调用该对象。(var)
- 暂时性死区(TDZ):变量在初始化之前不可引用,否则会报错。(let,const)
function test(){
console.log(a);
let a;
}
test();//报错:Cannot access 'a' before initialization
function test2(){
let b;
console.log(b);
}
test();//undefined
console.log(c);// Uncaught ReferenceError: c is not defined
let c;
console.log(b);//
var b;
console.log(b);
b = 1;
在执行上下文创建的时候,let
和const
定义的变量的值是没有初始化的,但是var
定义的变量的值会被直接初始化为 undefined
在执行 fn 时,会有以下过程(不完全):
- 进入 fn,为 fn 创建一个环境。
- 找到 fn 中所有用 var 声明的变量,在这个环境中「创建」这些变量(即 x 和 y)。
- 将这些变量「初始化」为 undefined。
- 开始执行代码
- x = 1 将 x 变量「赋值」为 1
- y = 2 将 y 变量「赋值」为 2
所以,var声明的变量在一开始就挂载到了词法环境当中,并且对应的标识符默认被赋值为undefined,也就是说var声明的对象,一开始就完成了完整的初始化。
而let声明的变量,虽然一开始标识符也挂载到词法环境当中了,但是标识符没有有赋值,还处在未初始化的状态,所以,let在初始化前是不能被访问的,从代码顺序上也就是在声明之前不能被访问。