深入理解 JavaScript的历史包袱——变量提升(Hoisting)

前言 :JavaScript 为什么在声明之前访问一个变量,有时返回 undefined,有时却直接报错?为什么函数能在定义之前被正常调用?这些"反直觉"的行为背后,藏着 JavaScript 引擎编译与执行 两阶段协作的根本原理。本文从一段令人困惑的代码出发,带你逐层拆解执行上下文、变量环境、词法环境,以及 var / let / const 各自不同的提升规则,让你吃透 Hoisting。


目录

  1. 引言:一段令人困惑的代码
  2. 什么是变量提升(Hoisting)
  3. 模拟变量提升:编译后的代码长什么样
  4. [JavaScript 代码执行流程全景](#JavaScript 代码执行流程全景 "#%E5%9B%9Bjavascript-%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B%E5%85%A8%E6%99%AF")
  5. [let / const 与 var 的分岔:暂时性死区(TDZ)](#let / const 与 var 的分岔:暂时性死区(TDZ) "#%E4%BA%94let--const-%E4%B8%8E-var-%E7%9A%84%E5%88%86%E5%B2%94%E6%9A%82%E6%97%B6%E6%80%A7%E6%AD%BB%E5%8C%BAtdz")
  6. 总结与最佳实践

一、引言:一段令人困惑的代码

先来看一段代码,你觉得它会输出什么?

javascript 复制代码
showName();
console.log(myName);
console.log(add);

var myName = '极客时间';

function showName() {
    console.log('函数showName被执行了');
}

var add = function(x, y) {
    return x + y;
};

按照"代码是一行一行顺序执行"的直觉,showName() 在第一行就被调用了,但函数定义在后面------这应该报错才对。myNameconsole.log 也在 var myName 声明之前,同样应该报错。

然而实际的运行结果是:

javascript 复制代码
函数showName被执行了
undefined
undefined
  • showName() 正常执行,没有报错。
  • myName 没有报错,但值是 undefined,而不是 '极客时间'
  • add 也没有报错,值同样是 undefined

这显然不是"一行一行执行"能解释的。JavaScript 到底对我们的代码做了什么?


二、什么是变量提升(Hoisting)

所谓变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎(如 Chrome 的 V8 引擎)把变量的声明部分和函数的声明部分提升到所在作用域顶部的"行为"。变量提升后,会给变量设置默认值 undefined

具体来说,提升的规则是:

  • var 声明的变量 :声明被提升到作用域顶部,默认值设为 undefined,赋值操作保留在原位置。
  • 函数声明(function foo() {...}:整体提升------声明和函数体一起被提升,值为函数对象。在声明之前调用完全没有问题。
  • 函数表达式(var add = function() {...} :这本质上是一个变量声明 + 赋值,所以只有变量名 add 被提升为 undefined,函数体并不会跟着提升。

让我们用一个例子来对比函数声明和函数表达式的区别:

javascript 复制代码
// 函数声明 ------ 整体提升,可以在声明前调用
foo();  // 正常执行
function foo() {
    console.log('foo');
}

// 函数表达式 ------ 只有变量名 bar 提升为 undefined
bar();  // TypeError: bar is not a function
var bar = function() {
    console.log('bar');
};

函数在 JavaScript 中是一等公民(First-Class Object),函数声明享有"整体提升"的特权,这是它和普通变量最大的不同。


三、模拟变量提升:编译后的代码长什么样

如果非要给变量提升找一个直观的理解方式,可以想象编译阶段把代码"重排"成了这样:

原代码:

javascript 复制代码
showName();
console.log(myName);
var myName = '极客时间';
function showName() {
    console.log('函数showName被调用了');
}

等效的"提升后"形态:

javascript 复制代码
// === 编译阶段提升的部分 ===
var myName = undefined;
function showName() {
    console.log('函数showName被调用了');
}

// === 执行阶段(原顺序) ===
showName();              // 函数showName被执行了
console.log(myName);     // undefined
myName = '极客时间';      // 赋值发生在这里

重要澄清 :变量和函数声明在代码中的物理位置并没有改变。上面的"重排"只是一种心智模型。实际上,提升发生在编译阶段,JS 引擎将这些声明提前放入内存(执行上下文)中,而不是真的去改动你的源代码。


四、JavaScript 代码执行流程全景

要真正理解变量提升,需要先理解 JavaScript 的执行机制。

一段 JavaScript 代码在执行前,会先经过 编译阶段 ,编译完成后才进入 执行阶段。编译阶段并不像 C/C++ 那样有独立的编译过程------它发生在代码执行前的"那一刹那"。

编译阶段的核心产出是两部分:

  1. 执行上下文(Execution Context)------代码运行所需的环境信息。
  2. 可执行代码------编译后的字节码,供执行阶段逐行运行。

执行上下文中有一个关键的结构:变量环境(VariableEnvironment) 。在编译阶段,var 声明的变量和函数声明就会被写入变量环境,并建立映射关系:

javascript 复制代码
VariableEnvironment:
    myName   → undefined
    showName → function() { console.log('函数showName被执行了') }

进入执行阶段后,引擎按顺序执行可执行代码。当遇到 myName = '极客时间' 时,才将变量环境中的 myName 更新为 '极客时间'

当代码中调用一个函数时,JavaScript 引擎会为该函数创建一个新的执行上下文,压入调用栈,并在其中重复"编译 → 执行"的过程。例如:

javascript 复制代码
showName();                           // 全局上下文
var myName = 'MT';
function showName() {
    var a = 1;                        // showName 自己的执行上下文
    console.log('函数showName被执行了');
}
  • 全局代码编译 → 全局执行上下文(VariableEnvironment: myName → undefined, showName → function
  • 执行到 showName() 时 → 创建 showName 的执行上下文 → 编译其内部代码 → a → undefined → 执行函数体
  • 函数执行完毕 → 弹出调用栈 → 回到全局上下文继续执行

五、let / const 与 var 的分岔:暂时性死区(TDZ)

ES6 引入了 letconst,它们的表现与 var 截然不同:

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

直接报错了!不是说"变量提升"吗------let 到底有没有提升?

答案是:let / const 也会在编译阶段完成内存分配,即它们也被"提升"了,但它们并不和 var 走同一条路。

执行上下文中,实际上有两套环境:

环境 存放内容 声明前访问
变量环境(VariableEnvironment) var 声明的变量、函数声明 可以访问,值为 undefined
词法环境(LexicalEnvironment) let / const 声明的变量 不可以访问,报错

letconst 声明的变量,从编译阶段完成内存分配到实际执行到声明语句之间,处于 暂时性死区(Temporal Dead Zone,TDZ)。在这段区域内,变量虽然已经分配了内存空间,但 JavaScript 引擎禁止你访问它。

进入执行阶段后,代码按顺序执行,遇到 let myName = '极客时间' 这行时,变量才"走出"死区,可以正常使用。

所以,"变量提升"这个术语严格来说是一个历史包袱。更准确的表述是:所有变量声明都在编译阶段完成了内存分配,但 var 允许在声明前访问(返回 undefined),而 let / const 不允许(抛出错)。 它们走的路径不同,规则也不同。


六、总结与最佳实践

回顾一下核心要点:

  1. 变量提升的本质 是 JavaScript 引擎在编译阶段将变量和函数声明提前放入内存(执行上下文的变量环境 / 词法环境)。代码执行阶段仍然是按顺序执行的。
  2. var + 函数声明 → 存入变量环境 → 声明前可访问(var 变量为 undefined,函数声明整体可用)。
  3. let / const → 存入词法环境 → 声明前处于暂时性死区(TDZ)→ 访问会直接报错。
  4. 函数表达式本质是变量,遵循变量的提升规则,而不是函数声明的规则。

在日常开发中:

  • 优先使用 const ,需要重新赋值时使用 let
  • 尽量避免使用 var------它的提升行为容易造成隐蔽的 bug,降低代码可读性。
  • 函数声明虽可提升,但为了代码清晰,尽量先声明再调用

理解变量提升,本质上是在理解 JavaScript 引擎的编译与执行机制。当你吃透了执行上下文、变量环境和词法环境这三者的关系,那些"反直觉"的行为就都有了清晰的解释。


------ 深入了解 JavaScript,从理解它的运行机制开始。

相关推荐
春日见2 小时前
五分钟入门 强化学习---Q-Learning算法与实现
人工智能·python·深度学习·算法·机器学习·计算机视觉
多年小白2 小时前
【周末消息】2026年5月30日-6月1日
大数据·人工智能·深度学习·机器学习·金融
丷丩2 小时前
MapLibre GL JS第31课:添加实时数据
javascript·gis·map·mapbox·maplibre gl js
weixin_468466852 小时前
图像连通域分析新手实战指南
图像处理·人工智能·深度学习·ai·机器视觉·连通域
candyTong3 小时前
Claude Code 每次调用 API 时,上下文是怎么"拼"出来的?
javascript·后端·架构
小林ixn3 小时前
别再背“变量提升”了!深入编译执行,彻底搞懂 JavaScript 运行机制
javascript
用户852495071843 小时前
为什么变量能 未定义先使用?
javascript·程序员
Larcher3 小时前
JS 变量提升:代码没动,为什么执行顺序就变了?
前端·javascript·前端框架
硅谷秋水3 小时前
世界动作模型:具身智能的下一前沿
大数据·人工智能·深度学习·计算机视觉·语言模型·机器人