【题目】关于var变量提升和函数声明提升

36 阅读6分钟

这是一个经典的JavaScript面试题,旨在考察对变量提升(Hoisting)、函数声明、以及var声明的理解。

题目

请写出以下代码的输出结果,并详细解释原因。

console.log(a);
console.log(foo);
console.log(bar);

var a = 'hello world';

function foo() {
    console.log('foo executed!');
}

var bar = function() {
    console.log('bar executed!');
}

期望的答案及解析

输出结果:

undefined
[Function: foo]
undefined

详细解析:

这段代码的执行结果是由 JavaScript 的  “变量提升”  机制决定的。

在代码正式执行之前,JavaScript 引擎会先进行一个“编译”阶段。在这个阶段,它会将所有的 变量声明 和 函数声明 提升到当前作用域的最顶部。

然而,提升的规则有所不同:

  1. var 变量的提升

    • 使用 var 声明的变量会被提升,但 只提升声明部分,不提升赋值部分

    • 在提升之后,到正式执行赋值语句之前,变量的值都是 undefined

    • 因此,对于 var a = ‘hello world’;

      • 提升后var a;(被移到作用域顶部,初始值为 undefined)
      • 原地保留a = ‘hello world’;(留在原地,等待执行)
    • 所以,在第一行 console.log(a) 执行时,a 已经被声明但尚未被赋值,因此它的值是 undefined

  2. 函数声明的提升

    • 使用 function 关键字声明的函数(函数声明)会 整体提升,包括函数名和函数体。

    • 因此,对于 function foo() {…}

      • 提升后:整个 foo 函数被移到了作用域顶部。
    • 所以,在第二行 console.log(foo) 执行时,foo 已经是一个完整的、可执行的函数了,因此输出函数的本体 [Function: foo]

  3. 函数表达式的提升(与 var 变量规则相同)

    • var bar = function() {…} 是一个函数表达式。它遵循的是 var 变量的提升规则,而不是函数声明的规则。
    • 提升后var bar;(被移到作用域顶部,初始值为 undefined)
    • 原地保留bar = function() {…};(留在原地,等待执行)
    • 所以,在第三行 console.log(bar) 执行时,bar 已经被声明(是一个变量),但尚未被赋值为那个函数,因此它的值也是 undefined

代码在引擎眼中的“执行顺序”:

理解了提升机制后,代码实际上是这样被处理的:

// 提升阶段:所有声明被“移动”到顶部
var a; // 声明提升,初始值 undefined
function foo() { // 函数声明整体提升
    console.log('foo executed!');
}
var bar; // 声明提升,初始值 undefined

// 执行阶段:按顺序执行代码
console.log(a); // 此时 a 的值为 undefined
console.log(foo); // 此时 foo 的值是完整的函数
console.log(bar); // 此时 bar 的值为 undefined

a = ‘hello world’; // 赋值操作开始执行

// bar 被赋值为一个函数,但前面的 console.log 已经执行过了
bar = function() {
    console.log('bar executed!');
}

考察点

  1. 基本概念:候选人是否理解 JavaScript 中的“提升”机制。
  2. 细节区分:候选人是否能清晰区分 函数声明 和 函数表达式 在提升时的不同行为。
  3. var 的陷阱:候选人是否了解 var 声明的变量在赋值前的状态是 undefined,这常常是程序中 bug 的来源。
  4. 执行上下文:这道题背后更深层次的知识是“执行上下文”的创建过程(其中包含创建变量对象、建立作用域链、确定 this 指向等),能引导到这里的候选人通常基础非常扎实。

进阶追问

如果候选人答对了,可以继续追问:

  • “如果将 var 改为 let 或 const,结果会有什么不同?为什么?”

    • 答案:使用 let 或 const 声明的变量也存在提升,但和 var 不同,它们被提升到了“暂时性死区”中。在声明语句之前访问它们会抛出一个 ReferenceError,而不是 undefined。这使代码更不易出错。

这个进阶问题可以考察候选人对 ES6 新特性以及其设计初衷的理解。

让我们修改一下代码,把 foo 函数名也改为 a

console.log(a);
console.log(a);

var a = 'hello world';

function a() {
    console.log('a executed!');
}

console.log(a);

输出结果及解析

输出结果:

[Function: a]
[Function: a]
hello world

详细解析:

当变量声明和函数声明同名时,提升的规则会更加复杂。关键在于理解提升的优先级覆盖关系。

提升的优先级:函数声明 > 变量声明

在提升阶段,JavaScript 引擎会按以下顺序处理同名的声明:

  1. 函数声明首先被提升:整个函数(包括函数体)被提升到最前面。
  2. 变量声明随后被提升:但如果该变量名已经被一个函数声明占用,那么这个变量声明会被忽略(因为函数声明的优先级更高)。
  3. 最后的赋值操作:留在原地,等待执行阶段处理。

代码在引擎眼中的“执行顺序”:

// 1. 首先提升函数声明(优先级最高)
function a() {
    console.log('a executed!');
}

// 2. 然后提升变量声明 var a;
// 但是,因为标识符 'a' 已经被函数声明占用,
// 所以这个重复的变量声明 `var a;` 会被 JavaScript 引擎静默忽略。
// 此处实际上没有任何操作

// ------------ 执行阶段开始 ------------
console.log(a); // 此时 a 的值是第1步创建的函数
console.log(a); // 同样,a 仍然是那个函数

a = 'hello world'; // 执行赋值操作:将字符串赋值给 a,覆盖了之前的函数引用

console.log(a); // 此时 a 的值已经被覆盖,变成了字符串 'hello world'

关键结论:

  1. 不会覆盖(在提升阶段) :后面的 var a 声明不会覆盖前面已经提升的 function a。函数声明的优先级更高。
  2. 会被覆盖(在执行阶段) :执行到 a = 'hello world' 这行赋值语句时,变量 a 的值才会被从函数覆盖为字符串。这是一个执行时的赋值操作,而不是声明阶段的提升。

如果交换顺序呢?

如果我们交换变量赋值和函数声明的位置:

console.log(a);

var a = 'hello world'; // 先遇到变量赋值

function a() {         // 后遇到函数声明
    console.log('a executed!');
}

console.log(a);

输出结果仍然是:

[Function: a]
hello world

原因:提升是在执行之前发生的,代码的书写顺序不影响提升的结果。无论你先写 var a 还是先写 function a,在提升阶段,永远是函数声明先被处理,同名的变量声明被忽略。执行阶段的赋值操作才会改变最终的值。

终极总结(非常重要)

当 var 变量声明和函数声明同名时:

  1. 提升优先级函数声明优先于变量声明。
  2. 提升结果:标识符最终会指向被提升的函数。
  3. 执行覆盖:如果后续有对该标识符的赋值操作(如 a = ...),则会用新值覆盖掉之前函数的引用。

这个知识点很好地展示了JavaScript语言的独特之处,也是面试中区分中级和高级候选人的常见题目。