JavaScript之声明提升(Hoisting)

浏览器解析JS脚本执行顺序问题

有些朋友可能会觉得JavaScript的代码是从上到下,一行一行的解释执行的。如果按照这样的思路,在有些情况下阅读代码会得到错误的结果,考虑以下代码:

1
2
3
a = 2;
var a;
console.log(a);

console.log(a)应该输出什么呢?有些开发者觉得会输出undefined,因为var a‘a = 2’之后,变量 a 被重复定义了,但是没有被赋值,所以是’undefined’。但是结果输出是 2。如下图所示:
"麻蛋,图片哪去啦~~~"
我们再来考虑另一段代码,如下所示:

1
2
console.log(a);
var a = 2;

这段代码会输出什么样的结果呢?有些人可能会觉得输出ReferenceError。因为变量a在没有声明的情况下就被使用了。真实结果呢,如下图所示:输出的是undefined
"麻蛋,图片哪去啦~~~"

为什么会这样呢?这就牵出了本文的主题:JavaScript声明提升

## JavaScript代码的运行规则 在JavaScript代码运行之前其实是有一个编译阶段的。编译之后才是从上到下,一行一行解释执行。**变量提升**就发生在编译阶段,它把变量和函数的声明提升至作用域的顶端。(编译阶段的工作之一就是将变量与其作用域进行关联)。 所以对于代码var a =2;来说,编译器看到的是两行代码var a; a = 2;
  • 第一个语句是声明语句,在编译阶段处理。// var a
  • 第二个语句是赋值语句,在运行阶段处理。// a = 2

那么我们再回过头来看看JS脚本执行顺序问题中出现的代码:
1
2
3
a = 2;
var a;
console.log(a);

应该这样来处理:

1
2
3
var a;            //编译阶段
a = 2; //运行阶段
console.log(a); //运行阶段

第二段代码:

1
2
console.log(a);
var a = 2;

应该这样来处理:

1
2
3
var a;            //编译阶段
console.log(a); //运行阶段
a = 2; //运行阶段

所以这段代码的最终输出结果是undefined

变量提升需要注意两点:

  1. 提升的部分只是变量声明,赋值语句和可执行的代码逻辑还保持在原地不动
  2. 提升只是将变量声明提升到变量所在的变量范围的顶端,并不是提升到全局范围,说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
foo();
function foo(){
console.log(a); //会输出undefined
var a = "2";
}
//变量提升之后的效果
function foo(){
var a;
console.log(a);
a = "2";
}
foo();

函数声明会提升,但是函数表达式就不了。

看如下代码:

1
2
3
4
5
6
foo();
var foo = function bar(){ //这是一个函数表达式,不再是函数声明。
console.log("bar");
}
//以上代码会报错:
Uncaught TypeError: foo is not a function

代码实质执行方式如下:

1
2
3
4
5
6
var foo;    
foo(); //TypeError,因为还没有赋值
bar(); //bar不可以在全局范围内引用
foo = function bar(){
console.log("bar");
}

函数是一等公民

变量声明函数声明都会得到变量提升,但函数声明会最先得到提升,然后是变量声明。
考虑如下代码:

1
2
3
4
5
6
7
8
foo();    //输出的结果为1
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}

代码实质执行方式如下:

1
2
3
4
5
6
7
function foo(){
console.log(1);
}
foo();
foo = function(){
console.log(2);
}

注意:var foo;由于是重复声明变量,所以被编译优化去掉。

对于函数声明来说,如果定义了相同的函数变量声明,后定义的声明会覆盖掉先前的声明,看如下代码:

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

JavaScript中是没有块级作用域的概念(ps:ES6中有改进了),看如下代码:

1
2
3
4
5
6
7
8
9
10
11
foo();    //输出结果为2
var a = true;
if(a){
function foo(){
console.log(1);
}
}else{
function foo(){
console.log(2);
}
}

这段代码输出结果为2,if语句没有块级作用域的功能,所以函数声明都被提升到全局作用域中,
又因为定义了两个foo,后来的定义覆盖了前边的定义,所以输出结果为2。

面试题解析

选择了网上三题‘较’难,迷惑的人较多的面试题:

题1:

1
2
3
4
5
console.log(foo);     // ?
console.log(bar); // ?

var foo = function(){...};
function bar(){...}

答案 : undefined ,function bar(){…}

解析:

1
2
3
4
5
6
7
var foo ;
function bar(){...}

console.log(foo); // undefined
console.log(bar); // function bar(){...}

foo = function(){...};

本题主要考的是函数和函数表达式的区别。
变量声明和函数先提升到顶部,赋值被留到原地,foo默认undefined。bar输出函数自己。

题2:

1
2
3
4
5
6
7
8
function foo(){
a = 5;
console.log(window.a); // ?
console.log(a); // ?
var a = 10;
console.log(a); // ?
}
foo();

答案 :undefined ,5 ,10
解析:
下面是本题的详细解析,考点就是提升和全局污染

1
2
3
4
5
6
7
8
9
10
11
function foo(){
var a ; // 因为下面有声明a变量,a的声明提前
a = 5; //因为在自己的作用域内有a的声明存在,a并不会污染到全局,
//而是绑定到本作用域的a上,这也是比较忽悠人的地方
console.log(window.a); // undefined
//a = 5 没有污染全局,所以window.a不存在,故输出undefined
console.log(a); // 5 ,a 的声明提升,变量 a = 10 没有提升,a 现在还是 5
a = 10;
console.log(a); // 10
}
foo();

这里涉及到全局污染问题,即不使用 var 或 其他声明关键字 去声明时,在本作用域找不到声明时,
默认向上级找,直到最顶层绑定到全局window上(严格模式报 not defined )。例如:

1
2
3
4
5
function foo(){
a = 1;
console.log(window.a); // 1 不使用 var定义变量a污染到了全局上
}
foo();

题3:

1
2
3
4
5
6
7
8
9
10
11
function foo() {
var a = 1;
function b() {
a = 10;
return '';
function a() {...}
}
b();
console.log(a); // ?
}
foo();

答案 :1
解析:
考点 1.污染 2.提升 3.作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function foo() {
var a ; // a 和 b 一起提升到作用域顶部

function b() {
function a() {...} //b里的函数a也提升到b的顶部
a = 10; //因为上面有变量a,所以a也不会污染到上一层,而是对函数a进行再次赋值
//如果函数执行,函数里的a的值是 10,且没有污染
return '';
}

a = 1; //对本作用域的a赋值

b(); //函数执行,b作用域内的a被赋值为10
console.log(a); // 1
// 这个有两点要搞清楚 :
// 1. b的a没有污染到这个作用域
// 2. 就近原则,本函数的log(a)找离自己最近的a变量,
//如果log在函数b内,那么输出 离自己最近的 10
}
foo();

Buy me a cup of coffee,thanks!