你真的理解 JavaScript 变量提升(Hoisting)吗?
从 V8 引擎编译原理深入剖析
很多人以为自己懂了变量提升,但真正理解其本质的人并不多。本文从 JavaScript 引擎的编译原理出发,带你彻底搞懂 Hoisting 的底层机制。
一、引言:那些年我们踩过的坑
看下面这段代码,你觉得结果是什么?
javascript
showName();
console.log(myName);
var myName = '极客时间';
function showName() {
console.log('函数showName被执行');
}
如果你的直觉是"这肯定报错",那你就掉进了 JavaScript 的第一个认知陷阱。运行结果是:
javascript
函数showName被执行
undefined
变量 myName 还没声明就能用?函数还没定义就能调?JS 代码不是一行一行执行的吗?
答案就藏在 变量提升(Hoisting) 里。
二、重新认识"代码按顺序执行"
传统认知中,代码从上到下逐行执行。这个理解对大多数语言没问题,但对 JavaScript 不完全正确。
JavaScript 代码的执行其实分为 两个阶段:
源代码 → 编译阶段(Compilation)→ 执行阶段(Execution)

- 编译阶段:JS 引擎(如 V8)在代码执行前,先把变量和函数的声明扫描一遍,在内存中分配空间
- 执行阶段:逐行执行代码,进行赋值、调用等操作
变量提升的本质,就是在编译阶段完成内存分配,而非把代码"物理移动"到顶部。
三、V8 引擎如何处理变量提升
3.1 编译阶段:创建执行上下文
当 V8 引擎遇到一段代码时,首先会创建 执行上下文(Execution Context)。执行上下文是代码执行的"环境",包含三部分:
| 组成部分 | 作用 |
|---|---|
| 变量环境(Variable Environment) | 存储 var 声明的变量和函数声明 |
| 词法环境(Lexical Environment) | 存储 let、const 声明的变量 |
| this 绑定 | 确定 this 的指向 |
编译阶段的核心工作就是:扫描代码,将变量和函数的声明分别放入对应的环境中。

3.2 变量提升的模拟
让我们手动模拟一下 V8 引擎对这段代码的处理:
javascript
// 源代码
showName();
console.log(myName);
var myName = '极客时间';
function showName() {
console.log('函数showName被执行');
}
编译阶段,V8 引擎做的事:
javascript
// 1. var 声明提升,值初始化为 undefined
var myName = undefined;
// 2. 函数声明完整提升(函数是一等对象,整个函数体都被放入内存)
function showName() {
console.log('函数showName被执行');
}
执行阶段,逐行执行:
javascript
showName(); // 函数已被提升,可以调用
console.log(myName); // myName 是 undefined
myName = '极客时间'; // 赋值操作
这就是为什么:
- 函数声明可以在定义前调用
var声明的变量在定义前访问不会报错,值为undefined
四、三种提升行为的差异
4.1 var 变量提升
javascript
var myName = undefined; // 编译阶段:只声明,赋值为 undefined
myName = '极客时间'; // 执行阶段:赋值操作

规则:只提升声明(declaration),不提升赋值(assignment)。
4.2 函数声明提升
javascript
function foo() {
console.log('函数foo被执行');
}
// 完整的函数声明,没有涉及赋值操作
// 编译阶段:整个函数体被提升到作用域顶部
规则:函数声明整体提升,包括函数体。这是唯一一种"完整提升"的声明。
4.3 函数表达式不提升
javascript
var bar = function() {
console.log('函数bar被执行');
};
// 编译阶段:只提升 var bar = undefined
// 执行阶段:才把函数赋值给 bar
javascript
bar(); // TypeError: bar is not a function
var bar = function() {
console.log('函数bar被执行');
};
规则 :函数表达式等同于 var + 赋值,只提升变量声明,不提升赋值。
五、函数提升 vs 变量提升:优先级之争
当函数名和变量名冲突时,谁说了算?
javascript
console.log(foo); // ƒ foo() { console.log('我是函数'); }
var foo = '我是变量';
function foo() {
console.log('我是函数');
}
结果:打印的是函数。
原因 :编译阶段,函数声明和 var 声明都会被提升,但函数声明的优先级更高。当两者冲突时,函数声明会覆盖 var 的 undefined 值。
但如果调换位置呢?
javascript
function foo() {
console.log('我是函数');
}
var foo = '我是变量';
console.log(foo); // '我是变量'
执行阶段的赋值操作会覆盖编译阶段的提升结果。执行阶段的赋值 > 编译阶段的提升。
六、let / const 与暂时性死区(TDZ)
let 和 const 是 ES6 引入的声明方式。它们也会被"提升",但行为完全不同。
javascript
console.log(myname); // ReferenceError: Cannot access 'myname' before initialization
let myname = '极客时间';
6.1 let/const 的提升机制
let 和 const 的声明也会在编译阶段被处理,但它们被放入 词法环境(Lexical Environment) 而非变量环境。
在词法环境中的变量,在声明语句执行前 不可访问 。这段时间被称为 暂时性死区(Temporal Dead Zone,TDZ)。
ini
let myname = '极客时间';
│← TDZ →│← 可访问 →│
↑
声明语句执行的时刻
6.2 为什么 let/const 要有 TDZ?
这是 JavaScript 引擎的一个设计决策,目的是:
- 防止在变量初始化前使用 :避免
undefined导致的逻辑错误 - 配合块级作用域:确保变量只在其块级作用域内有效
- 鼓励先声明后使用:提升代码的可读性和可维护性
七、经典面试题解析
题目 1
javascript
var foo = 1;
function bar() {
console.log(foo);
var foo = 2;
}
bar(); // undefined
解析 :函数 bar 内部有自己的作用域。编译阶段,bar 内部的 var foo 被提升到函数顶部,形成局部变量,覆盖了外部的 foo。执行时,打印的是局部变量 foo 的初始值 undefined。
题目 2
javascript
console.log(a); // ƒ a() { console.log(1); }
var a = 2;
function a() { console.log(1); }
console.log(a); // 2
解析:
- 编译阶段:
var a = undefined被提升,function a()也提升(优先级更高,覆盖undefined) - 执行阶段:
console.log(a)→ 打印函数;a = 2赋值;console.log(a)→ 打印2
题目 3
javascript
showName();
var showName;
console.log(typeof showName);
function showName() { console.log('1'); }
解析 :函数声明整体提升,所以 showName() 可以正常调用。但注意:后面的 var showName 不会重新声明,因为在编译阶段就已经被处理过了。
八、变量提升的影响与最佳实践
8.1 变量提升的问题
变量提升虽然是一种设计机制,但它确实带来了问题:
- 降低代码可读性:代码的实际执行顺序与书写顺序不一致
- 容易引发 bug:在变量声明前使用变量可能产生难以排查的错误
- 增加认知负担:开发者需要时刻记住"声明被提升"这个规则
8.2 最佳实践
javascript
// ❌ 不推荐:依赖变量提升
showName();
var showName = function() {};
// ✅ 推荐:先声明,后使用
var showName = function() {};
showName();
// ✅ 更推荐:使用 const/let,避免提升带来的困扰
const showName = function() {};
showName();
九、总结
| 声明方式 | 提升行为 | 初始值 | 作用域 |
|---|---|---|---|
var |
声明提升 | undefined |
函数作用域 |
let |
声明提升(TDZ) | 暂时不可访问 | 块级作用域 |
const |
声明提升(TDZ) | 暂时不可访问 | 块级作用域 |
| 函数声明 | 完整提升 | 函数体 | 函数作用域 |
| 函数表达式 | 只提升变量声明 | undefined |
取决于声明方式 |
核心要点:
- 变量提升是 JavaScript 引擎在 编译阶段 完成的行为,不是代码的"物理移动"
var声明只提升声明,不提升赋值;函数声明完整提升let/const存在暂时性死区(TDZ),在声明前不可访问- 函数提升的优先级高于
var提升,但执行阶段的赋值可以覆盖 - 现代 JavaScript 开发中,应优先使用
const和let,避免var带来的提升困扰
理解了变量提升的本质,你才算真正理解了 JavaScript 代码的执行机制。这不仅是面试的高频考点,更是日常开发中避免 bug 的基础。
参考资料:《你不知道的 JavaScript》(上卷)、Chrome V8 引擎执行机制