var、let、const是JavaScript声明变量的三种方式,其中let和const是ES6为JavaScript新增的两种方式,用法与var类似。曾经问到它们三者之间的区别时,答案之一就有let和const不存在变量提升。但是经过一番调查研究,发现这个答案并不是那么准确。
1. 变量提升
众所周知,var命令会发生变量提升的现象,即变量可以在声明前使用:
console.log(a); // undefined
var a = 1;
这种现象还是很奇怪的,因为按照正常的逻辑,变量应该在声明语句之后才可以使用。其实,JavaScript与其他语言一样,都需要经历编译和执行阶段。但JavaScript编译器在编译阶段会搜集所有的变量声明,并将变量声明提前到变量当前所在作用域的顶部,也就是说,变量声明在编译阶段已经执行,而赋值则在执行阶段执行到对应语句时才会执行。所以才会出现所谓的“变量提升”。上面代码等价于:
var a;
console.log(a); // undefined
a = 1;
为什么要强调当前呢?因为ES5分为全局作用域和函数作用域,不同作用域中同名变量互不影响。例如:
console.log(b); // ReferenceError: b is not defined
function foo () {
console.log(b); // undefined
var b = 1;
}
foo()
当代码执行时,在函数作用域内的变量b会被提升到当前作用域的顶部,也就是foo函数内的顶部,而不是整体代码的顶部,所以函数内输出undefined,而函数外部不存在变量b,所以会报ReferenceError错误。
隐式全局变量不会被提升:
function foo () {
console.log(b) // ReferenceError
b = 1
console.log(b) // 1
}
foo()
console.log(b) // 1
使用var声明变量,在函数内部是局部变量,在函数外部是全局变量,没有使用var声明的变量,在函数内部或外部都是全局变量,但如果是在函数内部声明,也叫隐式全局变量,在函数外部使用之前需要先调用方法,告知系统声明了全局变量后方可在函数外部使用。
另外,函数声明也会提升:
foo(); // 1
function foo () {
console.log(1)
}
函数表达式不会被提升
foo(); // TypeError: foo is not a function
var foo = function () {
console.log(1)
}
这是因为JavaScript编译器会在编译阶段优先读取函数声明的代码,以确保函数能够被引用到;而对于函数表达式,只有在执行到相应的语句时才进行解析。也可以这么理解,上述代码相当于声明了一个变量foo,然后把函数赋值给变量foo,而变量提升在最开始也说过是将变量的声明提升到顶部,赋值代码留在原地,所以函数表达式不会被提升。等价于:
var foo;
foo();
foo = function () {
console.log(1)
}
函数提升会优先于变量提升
console.log(foo); // [Function: foo]
var foo = 10;
function foo () {}
上述代码等价于:
function foo () {}
var foo;
console.log(foo);
foo = 10;
同名函数和变量为什么没有被覆盖呢?
这是因为对于同名的变量声明,Javascript采用的是忽略原则,后声明的会被忽略。对于同名的函数声明,Javascript采用的是覆盖原则,先声明的会被覆盖。对于同名的函数声明和变量声明,采用的是忽略原则,为了确保函数能够被引用到,在提升时函数声明会提升到变量声明之前,变量声明会被忽略,但是变量赋值以后会被覆盖。
同名变量:
//解析前
var a = 1;
var a =2;
// 解析后
var a;
var a; // 被忽略
a = 1;
a = 2;
同名函数:
function foo () {
console.log(1)
}
function foo () { // 覆盖前一个
console.log(2)
}
foo(); // 2
同名函数和变量:
// 解析前
console.log(foo); // [Function: foo]
var foo = 10;
function foo () {}
console.log(foo); // 10
// 解析后
function foo () {}
var foo; // 被忽略
console.log(foo);
foo = 10;
console.log(foo); //10
2. let和const
ES6中,为了纠正“变量提升”这一奇怪现象,let和const改变了语法行为,let和const所声明的变量必须要在声明后使用,否则便会报错:
console.log(a); // ReferenceError
let a = 1;
另外,let和const实际上为JavaScript新增了块级作用域的概念。通过let或const声明的变量只能在命令所在的代码块内有效。
let a = 1;
if (true) {
a = 2; // ReferenceError;
let a = 3;
}
在上述代码中,虽然if代码块外存在变量a,但是if代码块内let有声明了一个变量a,导致变量a被绑定在这个块级作用域内,不受外部的影响,所以在if代码块内部let声明前使用就会报错。同时,虽然let和const在相同作用域下不允许重复声明,但是由于块级作用域的存在,内层作用域不受外层作用域的影响,所以在不同作用域下可以定义同名变量。
3. let和const存在变量提升么?
弄明白了前两个大概念以后,正式来看看这个问题,通过标题2中的例子可以看出,在let声明变量前使用该变量,会抛出ReferenceError的错误,那么就说明let和const不会被提升么?
其实,let和const是会被提升的,准确的说是创建被提升了,但是初始化没有被提升。
根据ECMA-262中13.3.1的NOTE中所述:let and const declarations define variables that are scoped to the running execution context’s LexicalEnvironment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.
这段话的大概意思就是let和const定义了变量执行的词法环境,变量会在这个环境实例化时被创建,但是在变量的词法绑定之前不允许以任何方式对其进行访问......如果let声明的变量初始化时没有词法绑定,则分配一个未定义的值undefined,同时MDN上关于let介绍中也说到 var 和 let 的不同之处在于后者是在编译时才初始化。(词法环境应该是指上下文环境,而词法绑定应该就是赋值操作。)
也就说可以这么理解,let声明变量分为三部分:1.创建,2.初始化,3.赋值。 创建环节在当前环境实例化时完成。而通过const命令声明的是一个常量,一旦声明,常量的值(内存地址不能改变)就不能改变 ,所以const声明必须立即进行初始化,不能留到以后赋值,所以const声明变量分为两部分:1.创建,2.初始化,没有赋值操作,相当于把初始化赋值整合成了一步,在初始化的时候进行赋值。
总结:
根据上述ECMA-262中13.3.1和MDN文档还有标题1中所讲,通过var声明的变量,变量提升时相当于把创建和初始化进行了提升,没有提升赋值操作,可以理解为var命令声明变量其实为两部分:第一部分创建的同时进行初始化,第二部分赋值。 所以标题1中的代码会输出undefined:
// 通过var声明的变量,创建和初始化都进行提升,不提升赋值操作,所以被提升后初始化为undefined
console.log(a); // undefined
var a = 1;
而let和const仅仅提升了变量的创建,初始化及赋值操作都没有进行提升
if (true) {
a = 2; // ReferenceError;
let a = 3;
}
为了理解方便,可以将上述代码拆分成如下几步:
if (true) {
// 此时a的创建已经被提升到了if代码块内的顶部
a = 2; // ReferenceError; //此时对a进行赋值,由于a仅仅被创建,还没有初始化,所以会报错,a is not defined
let a; // 完成a的初始化,根据ECMA-262 此时a为undefined
a = 3; // 完成对a的赋值操作。
}
而let从创建被提升到初始化这中间的部分,就是我们平常所说的暂时性死区(TDZ),即在使用let命令声明变量初始化之前,该变量都是不可用的。
...
if (true) {
// a的创建被提升,TDZ的开始
a = 2; // ReferenceError;
let a; // 完成a的初始化,TDZ的结束
a = 3; // 完成对a的赋值操作。
}