变量声明提升

变量声明提升是JS中一个基础的问题,同时也是对JS词法作用域认识的一个提升。在JS面试题中,关于变量声明提升的问题还是占了不少比例的,另外,在码代码的时候可能也无意间因为这个原因产生错误而头疼好久。还有一个需要注意的是ES2015中let、const声明的变量不具备变量声明提升。

在《你不知道的JavaSript》上卷中,作者把变量声明提升这个问题比作“先有鸡还是先有蛋?”,我认为很形象。代码在执行的时候给人的感觉是一行一行的执行,这样可能比较符合我们的正常思维习惯,但是这实际上并不完全正确。为什么这样说呢?这就要引出JS在运行前其实有一个编译过程的这个问题,在编译阶段,JS引擎做了一些事使得代码并不是完全一行一行的执行了,而是将一些声明的代码顺序提前了,所以就产生了变量声明提升这个问题。

编译原理

想明白变量声明提升这个概念,JS编译原理必须清楚。
有些小伙伴认为JS不是一门编译动态脚本语言吗?怎么会有编译过程,其实在没有接触《你不知道的JavaSript》上卷这本书之前我也这么认为,直到读了这本书我才对JS的作用域和变量声明提升以及闭包等问题有了更清楚的认识,所以这里先向小伙伴们推荐一下这本书。

言归正传,接着说JS编译,JS的编译过程不是像其他语言的编译过程一样发生在构建之前的,大部分是在代码执行之前的几微米(甚至更短!)的时间内,那么这段时间内,编译器对我们的代码做了什么呢?我们通过一个例子来说明一下,比如var a =2;这条语句在会被JS引擎看成是两部分,分别为var a;a = 2。其中前一部分是发生在编译过程中,而第二部分发生在执行过程中。也就是说,编译的时候,JS引擎把我们对a的声明已经提前了。

为了更好地说明编译器的编译过程,我在举一个例子:

1
2
3
4
5
foo(2);
function foo(a){
console.log(a);
}

上面这段代码在执行的时候,JS引擎的工作过程是:

  1. 在编译阶段,首先遇到foo(2);一看这是个函数执行呀,这并不是我编译器的活呀,于是直接无视略过。
  2. 然后继续向下走,发现function,很明显是要声明一个函数(ES2015之前,声明变量只有var 和 function 这两个关键字,前者用于声明普通变量,后者用于声明函数或者方法),所以,JS引擎就会在当前作用域内的内存中开辟一块空间给foo;然后编译继续进行,这时候该对foo函数内部进行编译(从上到下一行一行编译),所以遇到形参a后,就在foo作用域的内存中为a开辟了一块空间,只不过此时a没有值,所以存的是undefined,继续向下走,没了—–结束。
  3. 现在开始执行阶段,首先遇到了foo(2);,开始干活:
    引擎:作用域,你见过foo没?
    作用域:见过,刚才编译器那小子刚声明了他,我给你。
    引擎:好的。那我来执行以下foo这个函数。
    引擎:作用域兄弟,你在foo中见过a吗?
    作用域:有,编译器也声明他了,给你。
    引擎:谢了哥们,我把2复制给他。

    …..

我发现虽然是简单描述了一下JS引擎的编译过程,好像已经莫名其妙的把变量声明提升给讲完了(尴尬。。。)。

变量声明提升

本文是用来记录变量声明提升的,结果在第一小节就通过JS引擎的编译过程就给讲完了。。。。。。

那这一小节就在再总结一下,顺便说一下函数优先吧!

变量声明提升的原因

  1. JS代码在执行之前有一个极其短的编译过程。
  2. 在这个过程中,JS引擎为var、function声明的变量和函数在当前作用域中分配内存空间。函数内的变量和嵌套函数也是一样,只不过分配的内存是其父函数作用域内的。
  3. JS引擎在执行的时候,通过询问作用域来查找有无该变量或者函数,然后执行相关赋值或者函数执行等操作。

函数优先

相信已经说清楚变量声明提升的原因了,但是还要注意一点就是,在编译过程中,如果var和function声明的变量为同一个,则function声明的优先级高于var声明的。来看一个例子吧!

1
2
3
4
5
6
7
8
9
10
11
foo();
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}

上面代码最终输出结果是1,你猜对了吗?上面的编译执行过程为:

  1. 编译阶段,从上往下编译,首先遇到foo();,直接无视略过。
  2. 遇到var foo;,在当前作用域内为foo分配内存空间,继续向下编译。
  3. 遇到function foo(){....},发现已经在当前作用域中声明了该变量,但是此时是function,优先级明显高于var,所以当前作用域中的foo变为function。
  4. 继续向下编译,发现foo=....很明显是个赋值操作吗,这是引擎的事,无视。
  5. 执行阶段,首先就遇到了foo();,于是询问当前作用域内有无foo的声明,发现有,还正好是个函数,那就别废话了,直接执行吧!于是控制台打印了1。
  6. 继续向下执行,略过var foo;function foo(){....},遇到一个复制操作,那就先在问问当前作用域有没有foo变量,有就赋值,没有就在全局中声明一个foo变量(非严格模式下);
  7. 好了,到此结束。

小结

变量声明是一个很基础的JS知识点,但如果没有编译这一步,可能理解上不好理解,但是有了编译这个过程后,相信就很容易了。最后留下一道题,检验一下自己:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var a = new Object();
a.param = 123;
function foo(){
get = function(){
console.log(1);
};
return this;
}
foo.get = function(){
console.log(2);
};
foo.prototype.get = function(){
console.log(3);
};
var get = function(){
console.log(4);
};
function get(){
console.log(5);
}
foo.get();
get();
foo().get();
get();
new foo.get();
new foo().get();
new new foo().get();

很经典的一道考察变量声明提升和原型链,还有操作符优先级的题。

测试功能而已,你非要赏点我就没办法了...