要回答这个问题,我们需要回头引用第一章关于编译器的讨论。回忆一下,引擎 实际上将会在它解释执行你的 JavaScript 代码之前编译它。编译过程的一部分就是找到所有的声明,并将它们关联在合适的作用域上。第二章向我们展示了这是词法作用域的核心。

    所以,考虑这件事情的最佳方式是,在你的代码的任何部分被执行之前,所有的声明,变量和函数,都会首先被处理。

    当你看到 var a = 2; 时,你可能认为这是一个语句。但是 JavaScript 实际上认为这是两个语句:var a;a = 2;。第一个语句,声明,是在编译阶段被处理的。第二个语句,赋值,为了执行阶段而留在 原处

    于是我们的第一个代码段应当被认为是这样被处理的:

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

    ……这里的第一部分是编译,而第二部分是执行。

    相似地,我们的第二个代码段实际上被处理为:

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

    所以,关于这种处理的一个有些隐喻的考虑方式是,变量和函数声明被从它们在代码流中出现的位置“移动”到代码的顶端。这就产生了“提升”这个名字。

    换句话说,先有蛋(声明),后有鸡(赋值)

    注意: 只有声明本身被提升了,而任何赋值或者其他的执行逻辑都被留在 原处。如果提升会重新安排我们代码的可执行逻辑,那就会是一场灾难了。

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

    函数 foo 的声明(在这个例子中它还 包含 一个隐含的、实际为函数的值)被提升了,因此第一行的调用是可以执行的。

    还需要注意的是,提升是 以作用域为单位的。所以虽然我们的前一个代码段被简化为仅含有全局作用域,但是我们现在检视的函数foo(..)本身展示了,var a被提升至foo(..)的顶端(很明显,不是程序的顶端)。所以这个程序也许可以更准确地解释为:

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

    函数声明会被提升,就像我们看到的。但是函数表达式不会。

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

    变量标识符 foo 被提升并被附着在这个程序的外围作用域(全局),所以 foo() 不会作为一个 ReferenceError 而失败。但 foo 还没有值(如果它不是函数表达式,而是一个函数声明,那么它就会有值)。所以,foo() 就是试图调用一个 undefined 值,这是一个 TypeError —— 非法操作。

    同时回想一下,即使它是一个命名的函数表达式,这个名称标识符在外围作用域中也是不可用的:

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

    这个代码段可以(使用提升)更准确地解释为:

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