JavaScript之声明提升

通常认为JavaScript代码是一句一句自上而下运行的,但事实是并非所有情况都是如此。《你不知道的JavaScript》书中,举了这么个例子,说明了代码执行并非都是自上而下的。

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

上述代码中,虽然a的声明在赋值之后,但控制台输出a的值并非为undefined,而是2。在来看下面的一段代码:

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

这时候控制台输出的值其实是undefined,为什么呢?部分人可能会认为输出结果应该是报异常了,或者是结果为2,但这两种结果都是错误的,这里扯到了JavaScript的编译,书中提到:“正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理 ”。所以不管声明位于哪个位置,在代码执行时都要被首先处理,即声明提升,在上述代码中,对于语句var a = 2,JavaScript其实会把它拆解为var aa = 2,其中var a定义声明会在编译阶段就被执行到[编译阶段],而a = 2则留在原地等待被执行[执行阶段],所以将上述代码拆解开来看,其实结果就很清晰了。

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

针对声明提升,书中也给出了一些总结:“只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码执行的 顺序,会造成非常严重的破坏 ”。上述声明提升属于变量声明提升,还有一种提升称为函数声明提升,可以参考下述代码:

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

控制输出了结果,说明了调用成功,说明就算函数声明定义在函数被调用时的后面[这句话绕,相信你可以领悟的],即foo( )位于函数声明之前,同样可以正常调用函数。书中还举了另一种例子,说明函数声明可以被提升,但是函数表达式并不可以被提升,可以参考下述代码:

1
2
3
4
foo(); // 不是ReferenceError, 而是TypeError!
var foo = function bar() {
// ...
};

这个例子说明虽然同样是函数,但存在不同,表达式并非可以做到提升,而函数声明可以做到。上述代码片段经过提升后,实际上会是以下这种形式:

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

对于函数声明和变量声明,函数声明优先于变量声明,函数会首先被提升,然后才是变量,可以观察下述代码:

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

输出结果为1,说明函数声明的确是优于变量声明的。引擎会将上述代码理解为:

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

还存在一种情况就是,后面的函数声明会将前面相同的函数声明覆盖掉,参考下述代码:

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 );
}

书中给出的小结:“我们习惯将var a = 2;看作一个声明,而实际上JavaScript引擎并不这么认为。它将var aa = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。 这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。 要注意避免重复声明,特别是当普通的var声明和函数声明混合在一起的时候,否则会引起很多危 险的问题!”。