前言 :JavaScript 为什么在声明之前访问一个变量,有时返回
undefined,有时却直接报错?为什么函数能在定义之前被正常调用?这些"反直觉"的行为背后,藏着 JavaScript 引擎编译与执行 两阶段协作的根本原理。本文从一段令人困惑的代码出发,带你逐层拆解执行上下文、变量环境、词法环境,以及var/let/const各自不同的提升规则,让你吃透 Hoisting。
目录
- 引言:一段令人困惑的代码
- 什么是变量提升(Hoisting)
- 模拟变量提升:编译后的代码长什么样
- [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")
- [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")
- 总结与最佳实践
一、引言:一段令人困惑的代码
先来看一段代码,你觉得它会输出什么?
javascript
showName();
console.log(myName);
console.log(add);
var myName = '极客时间';
function showName() {
console.log('函数showName被执行了');
}
var add = function(x, y) {
return x + y;
};
按照"代码是一行一行顺序执行"的直觉,showName() 在第一行就被调用了,但函数定义在后面------这应该报错才对。myName 的 console.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++ 那样有独立的编译过程------它发生在代码执行前的"那一刹那"。
编译阶段的核心产出是两部分:
- 执行上下文(Execution Context)------代码运行所需的环境信息。
- 可执行代码------编译后的字节码,供执行阶段逐行运行。
执行上下文中有一个关键的结构:变量环境(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 引入了 let 和 const,它们的表现与 var 截然不同:
javascript
console.log(myName); // ReferenceError: Cannot access 'myName' before initialization
let myName = '极客时间';
直接报错了!不是说"变量提升"吗------let 到底有没有提升?
答案是:let / const 也会在编译阶段完成内存分配,即它们也被"提升"了,但它们并不和 var 走同一条路。
执行上下文中,实际上有两套环境:
| 环境 | 存放内容 | 声明前访问 |
|---|---|---|
| 变量环境(VariableEnvironment) | var 声明的变量、函数声明 |
可以访问,值为 undefined |
| 词法环境(LexicalEnvironment) | let / const 声明的变量 |
不可以访问,报错 |

let 和 const 声明的变量,从编译阶段完成内存分配到实际执行到声明语句之间,处于 暂时性死区(Temporal Dead Zone,TDZ)。在这段区域内,变量虽然已经分配了内存空间,但 JavaScript 引擎禁止你访问它。
进入执行阶段后,代码按顺序执行,遇到 let myName = '极客时间' 这行时,变量才"走出"死区,可以正常使用。
所以,"变量提升"这个术语严格来说是一个历史包袱。更准确的表述是:所有变量声明都在编译阶段完成了内存分配,但
var允许在声明前访问(返回 undefined),而let/const不允许(抛出错)。 它们走的路径不同,规则也不同。
六、总结与最佳实践
回顾一下核心要点:
- 变量提升的本质 是 JavaScript 引擎在编译阶段将变量和函数声明提前放入内存(执行上下文的变量环境 / 词法环境)。代码执行阶段仍然是按顺序执行的。
var+ 函数声明 → 存入变量环境 → 声明前可访问(var变量为undefined,函数声明整体可用)。let/const→ 存入词法环境 → 声明前处于暂时性死区(TDZ)→ 访问会直接报错。- 函数表达式本质是变量,遵循变量的提升规则,而不是函数声明的规则。
在日常开发中:
- 优先使用
const,需要重新赋值时使用let。 - 尽量避免使用
var------它的提升行为容易造成隐蔽的 bug,降低代码可读性。 - 函数声明虽可提升,但为了代码清晰,尽量先声明再调用。
理解变量提升,本质上是在理解 JavaScript 引擎的编译与执行机制。当你吃透了执行上下文、变量环境和词法环境这三者的关系,那些"反直觉"的行为就都有了清晰的解释。
------ 深入了解 JavaScript,从理解它的运行机制开始。