前言:作为解释性语言的JS,在执行前会先进行预编译。理解了预编译,JS中的变量提升就可以更快的搞懂啦!
一、预编译前奏
在讲解预编译前,先讲解个知识点。敲黑板啦!小伙伴们不要光看,自己打打代码才知道。
- imply global:暗示全局变量:如果变量未经声明就赋值,此变量归全局对象所有,即归
window
所有。如下所示,即window.a = 10
。
这个
window
到底是什么,其实它就是一个全局对象,提供了一个全局作用域。在全局作用域声明变量,就是为全局对象添加属性。即在window对象中添加了一个属性a,其值为10。
<script type="text/javascript">
a = 10;
console.log(a);//访问这个a实际上就是访问window.a
<script>

window.a和window.b
会输出什么呢?
<script type="text/javascript">
function test() {
var a = b = 123;
}
test();
<script>

- b = 123;b是没有声明就赋值的,所以根据上面的imply global暗示全局变量,是一个全局变量,是
window
的一个属性,所以可以通过window.变量名访问
。 - var a = b;因为它在函数里,不是全局变量,是局部变量,所以在window中没有。
- 一切声明的全局变量,全是
window
的属性。
<script type="text/javascript">
var b = 20;
console.log(b);
<script>

一、预编译
看一段代码
- b输出的结果的
undefined
,那么undefined
是什么意思呢?是var
声明了变量但是未对它进行初始化。所以先让这个变量初始化为undefined
,也就是先让b=undefined
直到真正赋值阶段时才是20。 - 而a输出的是
Uncaught ReferenceError: Cannot access 'a' before initialization
。翻译过来的意思就是无法在初始化之前访问a。
<script type="text/javascript">
//var
console.log(b);//undefined
var b = 20;
//let
console.log(a);//Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 10;
<script>
为什么
var
和let
两者不一样呢?先来讲讲var
吧
var b = 20;
包括两个步骤:
- 声明:var b;
- 赋值:b = 20;
<script type="text/javascript">
var b;//该声明提升了,提升到作用域的顶部。
console.log(b);//undefined
b = 20;
<script>
var的提升其实是创建和初始化都提升了。也许你又会问那为什么初始化的值是undefined。这和执行前发生的预编译有关。
- 此提升过程在预编译中进行:
- 函数:预编译发生在函数执行前一刻
- 整个js :预编译发生在全局执行的前一刻
函数预编译步骤
先看下面的代码到底输出什么?既有函数又有变量,而且还同名,那变量的声明提升和函数提升到底谁的优先级高呢?想必是很懵逼吧,没关系,跟着我一起来一步一步理解下。
function test(a)
{
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
var b = function () {}
console.log(b);
function d() {}
}
test(1);
- 创建AO对象:
AO{}
- 找到形参和变量声明,将形参和变量声明作为AO的属性名,属性值为undefined:
AO{
形参或变量声明:undefined
}
也就是
AO{
a:undefined,//这里形参function test(a)和变量var a都是,写一次就好了
b:undefined
}
- 找到实参的值,赋值给形参
AO{
形参或变量声明:实参
}
也就是
AO{
a:1,
b:undefined
}
- 函数体里面找到函数声明, 值为函数体
AO{
形参或变量声明:函数体
}
也就是
AO{
a:function a() {},//函数声明
b:undefined,
//注意了 var b = function () {}这个不是函数声明,是函数表达式,所以b的值还是undefined
d:function d() {}//函数声明
}
- 此时AO对象已经创建完,那么就开始执行函数,开始打印。
注意:要打印的值都从预编译后的AO对象中取值
function test(a)
{
//1.输出function a() {}
console.log(a);
/*2. 预编译时已经变量提升了,但是赋值还没有操作。所以此时AO对象中a的值改变为123
AO{
a:123,
b:undefined,
d:function d() {}
}
*/
a = 123;
//3.输出:123
console.log(a);
function a() {}
//4.输出123
console.log(a);
/*5.改变AO对象中b的值
AO{
a:123,
b:function () {},
d:function d() {}
}
*/
var b = function () {}
//6.输出function () {}
console.log(b);
function d() {}
}
实际输出结果和分析的一样。棒棒哒!

三、提升
理解了预编译后,想必var
的变量提升弄明白了吧!var提升指的是创建和初始化都被提升了。所以下面代码输出的结果才是undefined
console.log(a);
var a = 20;
console.log(a);//Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 10;
对于上面的结果也许你会说:那let不就没有提升。这就错了,其实它是有提升的,只不过提升的只有创建过程,初始化时没有提升的。看码
let a = 1;
{
console.log(a);
let a = 2;
}
如果let不存在提升,那么应该输出1。但是却报错 Uncaught ReferenceError: Cannot access 'a' before initialization。这就说明let存在提升。只是被存在“暂存死区”中。只有当初始化后才能被引用。
四、总结
var
:【创建】和【初始化】都被提升function
:整体提升,也就是【创建】【初始化】【赋值】都被提升let
:【创建】过程提升,但初始化没有提升。