引言
你是否遇到过这样的代码:在变量声明之前使用它,却得到了 undefined 而不是报错?在函数声明之前调用它,却能正常执行?这背后就是 JavaScript 中著名的 变量提升(Hoisting) 现象。理解它的原理,能帮助我们写出更可预测、更健壮的代码。
本文将结合多个代码示例,深入剖析 JavaScript 的执行过程,涵盖编译阶段与执行阶段、函数优先提升、函数表达式与函数声明的区别,以及 let/const 与 var 的不同表现。
一、变量提升 ------ 直观现象
1.1 变量提升与函数提升
先来看一段代码:
ini
showName();
console.log(myName);
function showName() {
console.log('函数ShowName被执行了');
}
var myName = '极客时间';
运行结果:
showName()正常输出"函数ShowName被执行了"console.log(myName)输出undefined,而不是报错
为什么?因为 编译阶段 会先处理变量和函数的声明,将变量声明提升到作用域顶部(初始值为 undefined),函数声明则整体提升。
上述代码在编译后等价于:
javascript
// 编译阶段完成的内存分配
var myName = undefined;
function showName() {
console.log('函数ShowName被执行了');
}
// 执行阶段按顺序执行
showName();
console.log(myName); // undefined
myName = '极客时间';

1.2 函数是一等对象
在 JavaScript 中,函数是一等对象,可以被赋值给变量,也可以作为参数传递。这也导致了函数声明和函数表达式在提升行为上的差异。
二、编译阶段 vs 执行阶段
JavaScript 代码的执行分为两个阶段:
- 编译阶段 :变量和函数声明被处理,变量提升时分配内存并赋值为
undefined,函数声明提升时存储整个函数对象。 - 执行阶段:代码按顺序逐行执行,给变量真正赋值,调用函数等。
示例:完整的函数声明与函数表达式
javascript
var myName = undefined;
myName = '极客时间';
// 完整的函数声明 ------ 提升整个函数
function foo() {
console.log('foo');
}
// 函数表达式 ------ 只提升变量 bar,函数体不会提升
var bar = function () {
console.log('bar');
}
编译后,bar 变量被提升并初始化为 undefined,但右侧的函数表达式只有在执行阶段才会被赋值。

三、函数声明与变量声明的优先级
当同一个作用域内存在同名的函数声明和变量声明时,函数声明会优先于变量声明。
来看一个典型例子:
javascript
showName(); // 输出 1 还是 2 ?
var showName = function () {
console.log(2);
}
function showName() {
console.log(1);
}
实际输出是 1。因为编译阶段函数声明先被提升,随后变量声明(var showName)由于同名而被忽略(不会覆盖已存在的函数)。执行阶段先调用 showName(),输出 1。之后执行到 var showName = function() {...} 时,才对 showName 重新赋值为新函数。

四、多个同名函数声明:后者覆盖前者
如果存在多个同名的函数声明,最终生效的是最后一个。
scss
function showName() {
console.log('极客邦');
}
showName(); // 极客时间
function showName() {
console.log('极客时间');
}
showName(); // 极客时间
编译时,后面的函数声明会覆盖前面的,因此两次调用都输出 "极客时间"。
五、函数表达式不会提升函数体
与函数声明不同,函数表达式只提升变量名,函数体保留在赋值语句中。
javascript
showName(); // TypeError: showName is not a function
var showName = function () {
console.log('函数表达式');
}
编译后等价于:
ini
var showName = undefined;
showName(); // 此时 showName 不是函数,报错
showName = function() { ... };
所以,如果希望函数在定义前被调用,必须使用函数声明形式。
六、var 的"坑"与 let/const 的改进
很多开发者因为 var 的提升特性而写出难以调试的代码。ES6 引入的 let 和 const 改善了这一问题。
6.1 let 也提升,但存在暂时性死区(TDZ)
虽然 let 声明的变量也会被提升,但在声明之前访问它会抛出 ReferenceError,这是因为变量被提升但未被初始化,处于"暂时性死区"。
ini
console.log(myName); // ReferenceError: Cannot access 'myName' before initialization
let myName = '极客时间';
对比 var:
ini
console.log(myName); // undefined (不会报错)
var myName = '极客时间';
6.2 内存分配的环境不同
var声明的变量属于 变量环境 ,可以在声明前被访问(值为undefined)。let/const声明的变量属于 词法环境,在声明前不能访问。
编译阶段两者都会分配内存,但 let/const 的变量在进入作用域时并未初始化,只有执行到声明语句时才会被初始化。


总结
| 知识点 | 核心结论 |
|---|---|
| 变量提升 | var 提升并初始化为 undefined;let/const 提升但不初始化 |
| 函数声明提升 | 整个函数对象被提升,可在声明前调用 |
| 函数表达式提升 | 只提升变量名,函数体不会提升,调用时需在赋值之后 |
| 优先级 | 函数声明优先于变量声明,同名变量声明被忽略 |
| 同名函数 | 后面的函数声明会覆盖前面的 |
| 编译 vs 执行 | 编译阶段做提升内存分配,执行阶段按顺序运行代码 |
| 暂时性死区 (TDZ) | let 和 const 声明前不可访问,否则报错 |
理解了编译阶段与执行阶段的分离,以及不同声明方式的提升差异,你就能轻松解释诸如"函数提升优先于变量"、"为什么 let 没有变量提升错觉"等问题。希望这篇文章能帮你彻底掌握 JavaScript 的执行原理,写出更加可靠的代码。
本文基于对 JavaScript 引擎底层行为的总结,所有示例均可直接在浏览器控制台或 Node.js 中运行验证。