# 变量提升

(翻译自You Don't Know JS (opens new window))

来看看下面的代码:

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

大多数人或许会认为最后显示 undefined ,因为 var a 在赋值之后,所以就会觉得变量被重新定义为默认值 undefined ,不过结果应该是显示 2

下一个例子:

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

有的人可能认为会输出 2,还有的人可能会认为在初始化之前调用,会抛出引用错误。都错了,这里会输出 undefined

这到底是为什么?到底是先有鸡(赋值)还是先有蛋(声明)?

# 看看编译器做了什么

为了从原理上说明问题,我们需要了解编译器做了什么。

当你看到 var a=2; 时,你可能觉得它是一个语句,但是实际上是被作为两句话来处理:var aa=2。第一句是声明,会在编译阶段被处理;第二句是赋值,会在执行阶段放到合适的位置。

所以上面第一个例子实际上会变成:

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

同样的第二个例子实际上会变成:

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

简而言之就是蛋(声明)在鸡(赋值)之前。

注意:只有声明本身被提前,而任何赋值或其他可执行逻辑都保留在适当的位置。变量提升不会改变原有代码的执行逻辑。

再看一个例子:

foo();

function foo() {
  console.log(a); // undefined

  var a = 2;
}
1
2
3
4
5
6
7

foo 的声明被提升了,所以最前面的函数调用可以执行。

还有个很重要的点需要注意,提升的范围是作用域,foo 里面的var a会被提升到函数内部的最前面,而不是全局作用域的最顶部,上面的代码会转换成:

function foo() {
  var a;

  console.log(a); // undefined

  a = 2;
}

foo();
1
2
3
4
5
6
7
8
9

函数声明会被提升,但是函数表达式则不会:

foo(); // not ReferenceError, but TypeError!

var foo = function bar() {
  // ...
};
1
2
3
4
5

因为变量 foo 会被提升所以不会产生引用错误,但是函数表达式不会,所以变量的值是 undefined ,也就会产生类型错误。

而且就算是命名的函数表达式也不会提升:

foo(); // TypeError
bar(); // ReferenceError

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

# 函数优先

函数和变量声明都会被提升,到底哪个会更优先呢,看个例子:

foo(); // 1

var foo;

function foo() {
  console.log(1);
}

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

会被转换成:

function foo() {
  console.log(1);
}

foo(); // 1

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

可以看到变量声明在函数之前,但是函数声明被提升到普通变量之前,当多个重复var声明,多余的会被忽略,多个函数声明,后面的会覆盖前面的:

foo(); // 3

function foo() {
  console.log(1);
}

var foo = function() {
  console.log(2);
};

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

# let

也和 var 一样会被提升,不过在初始化之前会被提升到 TDZ,在这时访问变量会报引用错误,所以看起来好像没提升一样。