这是一个经典的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 引擎会先进行一个“编译”阶段。在这个阶段,它会将所有的 变量声明 和 函数声明 提升到当前作用域的最顶部。
然而,提升的规则有所不同:
-
var变量的提升:-
使用
var声明的变量会被提升,但 只提升声明部分,不提升赋值部分。 -
在提升之后,到正式执行赋值语句之前,变量的值都是
undefined。 -
因此,对于
var a = ‘hello world’;:- 提升后:
var a;(被移到作用域顶部,初始值为undefined) - 原地保留:
a = ‘hello world’;(留在原地,等待执行)
- 提升后:
-
所以,在第一行
console.log(a)执行时,a已经被声明但尚未被赋值,因此它的值是undefined。
-
-
函数声明的提升:
-
使用
function关键字声明的函数(函数声明)会 整体提升,包括函数名和函数体。 -
因此,对于
function foo() {…}:- 提升后:整个
foo函数被移到了作用域顶部。
- 提升后:整个
-
所以,在第二行
console.log(foo)执行时,foo已经是一个完整的、可执行的函数了,因此输出函数的本体[Function: foo]。
-
-
函数表达式的提升(与
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!');
}
考察点
- 基本概念:候选人是否理解 JavaScript 中的“提升”机制。
- 细节区分:候选人是否能清晰区分 函数声明 和 函数表达式 在提升时的不同行为。
var的陷阱:候选人是否了解var声明的变量在赋值前的状态是undefined,这常常是程序中 bug 的来源。- 执行上下文:这道题背后更深层次的知识是“执行上下文”的创建过程(其中包含创建变量对象、建立作用域链、确定
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. 首先提升函数声明(优先级最高)
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'
关键结论:
- 不会覆盖(在提升阶段) :后面的
var a声明不会覆盖前面已经提升的function a。函数声明的优先级更高。 - 会被覆盖(在执行阶段) :执行到
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 变量声明和函数声明同名时:
- 提升优先级:函数声明优先于变量声明。
- 提升结果:标识符最终会指向被提升的函数。
- 执行覆盖:如果后续有对该标识符的赋值操作(如
a = ...),则会用新值覆盖掉之前函数的引用。
这个知识点很好地展示了JavaScript语言的独特之处,也是面试中区分中级和高级候选人的常见题目。