什么是变量提升
变量提升是一个将变量或者声明函数提升到作用域起始处的过程。 通常指的是变量声明var
和函数声明function fun(){...}
先有鸡还是先有蛋?
直觉上会认为JavaScript代码在执行时是由上到下一行一行执行的。但实际上并不完全是这样的,有一种情况会导致这个假设是错误的,这种情况就是变量提升的情况。
看下面这段代码:
a = 2;
var a;
console.log(a);
你认为console.log(a)
会输出什么? 很多开发者会认为是undefined
,因为var a
声明在a = 2
之后,他们自然而然的认为变量被重新赋值了,因此会被赋予默认值undefined
,但实际输出的结果是2
看另外一段代码:
console.log(a);
var a = 2;
鉴于上一段代码片段所表现出来的某种非自上而下的行为特点,你可能会认为这个代码片段也会有同样的行为输出2
。 还有人可能会认为,由于变量a
在使用前没有先声明,因此会抛出ReferenceErrot
异常。
不幸的是以上这两种猜测都是不对的。输出来的结果是undefined
这是为什么呢?到底发生了什么?看来我们面对的是一个先有鸡还是先有蛋的问题。到底是声明(蛋)在前,还是赋值(鸡)在前?
为了解释上面的这些问题,我们来学习一些编译器相关的内容。
编译器相关内容
为了搞明白上面的问题,需要知道浏览器引擎在解释JavaScript代码之前首先会对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。
因此,真确的思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理
。
当你看到var a = 2;
时,你可能会认为这是一个声明。但JavaScript实际上会将其看成两个声明:var a;
和 a = 2
; 第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行。
上面第一个代码片段会以如下形式进行处理:
var a;
a = 2;
console.log(a); //2
其中第一部分是编译,而第二部分是执行。
类似的,我们的第二个代码片段实际是按照下面流程处理的:
var a;
console.log(a); //undefined
a = 2;
因此,打个比方,这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了当前作用域的最上面。这个过程就叫作提升
因此在js里面是先有蛋(声明)后有鸡(赋值)
只有声明本身会被提升,而赋值或者其他运行逻辑会留在原地。如果提升改变了代码执行的顺序,会造成非常严重的破坏。
变量声明和函数声明都会被提升,那他们哪个的优先级会更高呢?
答案是:函数会首先被提升,然后才是变量
思考下面这段代码会输出什么:
foo();
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}
输出的是1
,而不是2
实际上上面的代码会被引擎理解为如下形式:
function foo(){
console.log(1);
}
foo(); //1
foo = function(){
console.log(2);
}
注意,var foo
尽管会出现在function foo()...
的声明之前,但它是重复的声明,因此被忽略了,因为函数声明会被提升到普通变量之前,也就是相同名称声明的函数和变量,函数的会被提升,函数的优先级要高。
虽然变量和函数可以重复声明,但是在同一个作用域中进行重复定义是非常糟糕的,而且经常会导致各种奇怪的问题,应该尽量避免
let不存在变量提升
let(以及具备了和 let 相似声明行为的 const 和 class)都不存在变量提升问题。详情请看为什么 let 不存在变量提升
技术参考
- 《你不知道的JavaScript(上)》