你真的理解 JavaScript 变量提升(Hoisting)吗?从 V8 引擎编译原理深入剖析

你真的理解 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) 存储 letconst 声明的变量
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 声明都会被提升,但函数声明的优先级更高。当两者冲突时,函数声明会覆盖 varundefined 值。

但如果调换位置呢?

javascript 复制代码
function foo() {
    console.log('我是函数');
}

var foo = '我是变量';

console.log(foo); // '我是变量'

执行阶段的赋值操作会覆盖编译阶段的提升结果。执行阶段的赋值 > 编译阶段的提升


六、let / const 与暂时性死区(TDZ)

letconst 是 ES6 引入的声明方式。它们也会被"提升",但行为完全不同。

javascript 复制代码
console.log(myname); // ReferenceError: Cannot access 'myname' before initialization
let myname = '极客时间';

6.1 let/const 的提升机制

letconst 的声明也会在编译阶段被处理,但它们被放入 词法环境(Lexical Environment) 而非变量环境。

在词法环境中的变量,在声明语句执行前 不可访问 。这段时间被称为 暂时性死区(Temporal Dead Zone,TDZ)

ini 复制代码
let myname = '极客时间';
│← TDZ →│← 可访问 →│
         ↑
    声明语句执行的时刻

6.2 为什么 let/const 要有 TDZ?

这是 JavaScript 引擎的一个设计决策,目的是:

  1. 防止在变量初始化前使用 :避免 undefined 导致的逻辑错误
  2. 配合块级作用域:确保变量只在其块级作用域内有效
  3. 鼓励先声明后使用:提升代码的可读性和可维护性

七、经典面试题解析

题目 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

解析

  1. 编译阶段:var a = undefined 被提升,function a() 也提升(优先级更高,覆盖 undefined
  2. 执行阶段: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 取决于声明方式

核心要点

  1. 变量提升是 JavaScript 引擎在 编译阶段 完成的行为,不是代码的"物理移动"
  2. var 声明只提升声明,不提升赋值;函数声明完整提升
  3. let/const 存在暂时性死区(TDZ),在声明前不可访问
  4. 函数提升的优先级高于 var 提升,但执行阶段的赋值可以覆盖
  5. 现代 JavaScript 开发中,应优先使用 constlet,避免 var 带来的提升困扰

理解了变量提升的本质,你才算真正理解了 JavaScript 代码的执行机制。这不仅是面试的高频考点,更是日常开发中避免 bug 的基础。


参考资料:《你不知道的 JavaScript》(上卷)、Chrome V8 引擎执行机制

相关推荐
蜡台1 小时前
Vue2 使用 typescript 教程
前端·vue.js·typescript
光影少年2 小时前
Redux Toolkit 用法、解决原生Redux 冗余问题
开发语言·前端·javascript·react.js·中间件·前端框架·ecmascript
云水一下2 小时前
JavaScript 从零基础到精通系列:DOM 操作与事件驱动编程
前端·javascript
努力发光的程序员2 小时前
面试官与程序员谢飞机的3轮Java大厂面试问答实录:涵盖Spring Boot、微服务与数据库技术
java·jvm·spring boot·redis·面试·hibernate·microservices
ZC跨境爬虫2 小时前
跟着 MDN 学CSS day_32:(Web字体深度解析与实践指南)
前端·javascript·css·ui·html
Rick19932 小时前
Redis 高频面试 10 题
数据库·redis·面试
砍材农夫2 小时前
物联网 基于netty核心实战-安全tls
java·开发语言·前端·物联网·安全
SEO_juper2 小时前
JavaScript 渲染:AI 智能体无法读取,直接影响收录
开发语言·前端·javascript·aigc·seo·跨境电商·geo