变量提升和函数声明提升

JavaScript的执行步骤

要讨论JavaScript中的变量提升和函数声明提升,我们首先要知道的是,当浏览器拿到一段JavaScript代码的时候并不会去直接执行它,浏览器引擎会在执行JavaScript代码之前对其进行预编译。总的而言就是下列两个步骤:

  1. javascript预编译:就是通过语法分析和预解析构造合法的语法分析树,读取变量和函数的声明,并确定其作用域即生效范围。
  2. javascript执行:执行具体的代码,JavaScript引擎在执行每个函数实例时,都会创建一个执行环境和活动对象(它们属于宿主对象,与函数实例的生命周期保持一致)
变量提升

预编译阶段的一部分工作就是找到所有的变量和函数声明,并用合适的作用域与之关联起来。因此,包括变量和函数在内的所有声明都会在其他代码执行前首先被处理,例如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
console(a) //输出undefined;
var a = 1;

---------------------------------
/*
但其实真实的步骤是这样的
会将声明提前到最顶部
而赋值依然会停留在原地
*/
var a;
console.log(a); //输出undefined
a = 1;

在预编译阶段会将所有的变量声明和函数声明从它们在原本的位置移动到了最上面。而这个过程就叫做提升。而且要注意的是只有声明会被提升,而赋值或其他运行逻辑会留在原地。具体的步骤如下:

  1. 将var a 提升至作用域的最顶部。
  2. 初始化a为undefined。
  3. 打印a。
  4. 给a赋值为1。

而且要注意的是每个作用域都会在各自的作用域内进行提升操作:

1
2
3
4
5
6
7
8
console.log(name); //输出undefined
function say(){
console.log(name); //输出:undefined
var name = 'Bob';
console.log(name); //输出:'Bob'
};
say();
var name = 'Jake';

真实的顺序是这样的:

1
2
3
4
5
6
7
8
9
10
var name;
console.log(name); //输出undefined
function say(){
var name;
console.log(name); //输出:undefined
name = 'Bob';
console.log(name); //输出:'Bob'
};
say();
name = 'Jake';
函数声明提升

另外需要注意Javascript中函数的函数声明方式存在的坑。函数声明在提升的时候,实际上会把整个函数提升上去,包括函数定义的部分,所以这么做并不会报错:

1
2
3
4
5
fn();
function fn(){
console.log(a); //输出undefined
var a = 1;
};

而对于函数表达式,与定义其它基本类型变量一样,逐句执行并解析,因此会报错:

1
2
3
4
fn();   //不是ReferenceError,而会是TypeError!
var fn = function(){
//函数表达式
};

另外要记住的是,即使是具名的函数表达式,名称标识符在赋值之前也无法再所在作用域中使用:

1
2
3
4
5
fn();  //TypeError
vm(); //ReferenceError
var fn = function vm() {
//一些有意思的东西
};

上面的代码在进行提升后实际上是这样的:

1
2
3
4
5
6
7
var fn;
fn(); //TypeError
vm(); //ReferenceError
fn = function() {
var vm = ...self...
//一些有意思的东西
};
函数优先

当变量声明和函数声明同时被提升时,函数会首先被提升,然后才是变量(尤其是当有多个重复声明的代码中尤为重要)。我们来看看下面的情况:

1
2
3
4
5
6
7
8
fn();  //1
var fn;
fn = function() {
console.log(2);
};
function fn() {
console.log(1);
};

如上所见,当两者同时出现时,函数声明会被提升到最前面,然后再是变量提升(当重复声明时会被忽略)。JavaScript引擎具体的理解如下:

1
2
3
4
5
6
7
8
function fn() {
console.log(1);
};
// var fn;在此,但由于重复声明被忽略
fn(); //1
fn = function() {
console.log(2);
};

除此之外,后面出现的函数声明还可以覆盖前面的函数声明:

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

从上面的代码我们可以看到,在同一个作用域下重复的定义是很糟糕的,经常会导致一些出乎意料的问题,因此我们要避免这样!

块级作用域内部的函数声明

块级作用域内部的函数声明通常也会被提升到所在作用域的顶部,且这个过程是不被条件判断所控制的:

1
2
3
4
5
6
7
8
9
10
11
fn();  // 2
var a = ture;
if(a) {
function fn() {
console.log(1);
}
else {
function fn() {
consloe.log(2);
}
}

这个过程是不可控的。因此原先的ES5中,规定不能在块级作用域内进行函数声明,只能在全局作用域与函数作用域内声明,但又由于要兼容以往的ES3等,所以在出现上述的情况下,浏览器不会报错。但在ES6中,函数声明是可以在块级作用域中进行声明的,但需要注意以下三点:

  1. 在严格模式下函数声明会被提前到块级作用域头部。
  2. 在非严格模式下,函数声明会被提升至外围函数或全局作用域的顶部。
  3. 函数声明只在有大括号的块级作用域才能使用,不然会报错。

(另外在ES6中的let和const由于暂存死区的原因,不会发生变量提升,这需要注意)