变量声明提升是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的声明已经提前了。
为了更好地说明编译器的编译过程,我在举一个例子:
foo(2);
function foo(a){
console.log(a);
}
上面这段代码在执行的时候,JS引擎的工作过程是:
- 在编译阶段,首先遇到
foo(2);
一看这是个函数执行呀,这并不是我编译器的活呀,于是直接无视略过。 - 然后继续向下走,发现
function
,很明显是要声明一个函数(ES2015之前,声明变量只有var 和 function 这两个关键字,前者用于声明普通变量,后者用于声明函数或者方法),所以,JS引擎就会在当前作用域内的内存中开辟一块空间给foo
;然后编译继续进行,这时候该对foo函数内部进行编译(从上到下一行一行编译),所以遇到形参a后,就在foo作用域的内存中为a开辟了一块空间,只不过此时a没有值,所以存的是undefined,继续向下走,没了-----结束。 - 现在开始执行阶段,首先遇到了
foo(2);
,开始干活:
引擎:作用域,你见过foo
没?
作用域:见过,刚才编译器那小子刚声明了他,我给你。
引擎:好的。那我来执行以下foo这个函数。
引擎:作用域兄弟,你在foo中见过a吗?
作用域:有,编译器也声明他了,给你。
引擎:谢了哥们,我把2复制给他。
.....
我发现虽然是简单描述了一下JS引擎的编译过程,好像已经莫名其妙的把变量声明提升给讲完了(尴尬。。。)。
变量声明提升
本文是用来记录变量声明提升的,结果在第一小节就通过JS引擎的编译过程就给讲完了。。。。。。
那这一小节就在再总结一下,顺便说一下函数优先吧!
变量声明提升的原因
- JS代码在执行之前有一个极其短的编译过程。
- 在这个过程中,JS引擎为var、function声明的变量和函数在当前作用域中分配内存空间。函数内的变量和嵌套函数也是一样,只不过分配的内存是其父函数作用域内的。
- JS引擎在执行的时候,通过询问作用域来查找有无该变量或者函数,然后执行相关赋值或者函数执行等操作。
函数优先
相信已经说清楚变量声明提升的原因了,但是还要注意一点就是,在编译过程中,如果var和function声明的变量为同一个,则function声明的优先级高于var声明的。来看一个例子吧!
foo();
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}
上面代码最终输出结果是1,你猜对了吗?上面的编译执行过程为:
- 编译阶段,从上往下编译,首先遇到
foo();
,直接无视略过。 - 遇到
var foo;
,在当前作用域内为foo分配内存空间,继续向下编译。 - 遇到
function foo(){....}
,发现已经在当前作用域中声明了该变量,但是此时是function
,优先级明显高于var
,所以当前作用域中的foo
变为function。 - 继续向下编译,发现
foo=....
很明显是个赋值操作吗,这是引擎的事,无视。 - 执行阶段,首先就遇到了
foo();
,于是询问当前作用域内有无foo的声明,发现有,还正好是个函数,那就别废话了,直接执行吧!于是控制台打印了1。 - 继续向下执行,略过
var foo;
和function foo(){....}
,遇到一个复制操作,那就先在问问当前作用域有没有foo变量,有就赋值,没有就在全局中声明一个foo变量(非严格模式下); - 好了,到此结束。
小结
变量声明是一个很基础的JS知识点,但如果没有编译这一步,可能理解上不好理解,但是有了编译这个过程后,相信就很容易了。最后留下一道题,检验一下自己:
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();
很经典的一道考察变量声明提升和原型链,还有操作符优先级的题。
网友评论