要回答这个问题,我们需要回头引用第一章关于编译器的讨论。回忆一下,引擎 实际上将会在它解释执行你的 JavaScript 代码之前编译它。编译过程的一部分就是找到所有的声明,并将它们关联在合适的作用域上。第二章向我们展示了这是词法作用域的核心。
所以,考虑这件事情的最佳方式是,在你的代码的任何部分被执行之前,所有的声明,变量和函数,都会首先被处理。
当你看到 var a = 2;
时,你可能认为这是一个语句。但是 JavaScript 实际上认为这是两个语句:var a;
和 a = 2;
。第一个语句,声明,是在编译阶段被处理的。第二个语句,赋值,为了执行阶段而留在 原处。
于是我们的第一个代码段应当被认为是这样被处理的:
var a;
a = 2;
console.log( a );
……这里的第一部分是编译,而第二部分是执行。
相似地,我们的第二个代码段实际上被处理为:
var a;
console.log( a );
a = 2;
所以,关于这种处理的一个有些隐喻的考虑方式是,变量和函数声明被从它们在代码流中出现的位置“移动”到代码的顶端。这就产生了“提升”这个名字。
换句话说,先有蛋(声明),后有鸡(赋值)。
注意: 只有声明本身被提升了,而任何赋值或者其他的执行逻辑都被留在 原处。如果提升会重新安排我们代码的可执行逻辑,那就会是一场灾难了。
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
函数 foo
的声明(在这个例子中它还 包含 一个隐含的、实际为函数的值)被提升了,因此第一行的调用是可以执行的。
还需要注意的是,提升是 以作用域为单位的。所以虽然我们的前一个代码段被简化为仅含有全局作用域,但是我们现在检视的函数foo(..)
本身展示了,var a
被提升至foo(..)
的顶端(很明显,不是程序的顶端)。所以这个程序也许可以更准确地解释为:
function foo() {
var a;
console.log( a ); // undefined
a = 2;
}
foo();
函数声明会被提升,就像我们看到的。但是函数表达式不会。
foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
// ...
};
变量标识符 foo
被提升并被附着在这个程序的外围作用域(全局),所以 foo()
不会作为一个 ReferenceError
而失败。但 foo
还没有值(如果它不是函数表达式,而是一个函数声明,那么它就会有值)。所以,foo()
就是试图调用一个 undefined
值,这是一个 TypeError
—— 非法操作。
同时回想一下,即使它是一个命名的函数表达式,这个名称标识符在外围作用域中也是不可用的:
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
这个代码段可以(使用提升)更准确地解释为:
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...self...
// ...
}