拆穿 JavaScript 变量提升的"魔术"------从一段反直觉代码说起
一、开场先看一段"反直觉"的代码
ini
showName();
console.log(myName);
console.log(add);
var myName = '极客时间';
function showName() {
console.log('函数showName被执行了');
}
var add = function (x, y) {
return x + y;
};
如果你刚学 JavaScript,你的大脑大概会这样运转:
"showName 还没定义怎么就调用了?myName 还没声明就 console.log?这不全得报错吗?"
来,按一下回车------
javascript
函数showName被执行了
undefined
undefined
不仅没报错,showName() 居然还正确执行了 。myName 也没报错,只是乖乖地输出了 undefined。
这就像你在餐厅点了一份还没上菜单的菜,厨师不仅没赶你走,还真给你端了一盘"空气炒肉"------盘子来了,肉还在锅里。
这就是 JavaScript 圈里大名鼎鼎的「变量提升 (Hoisting)」。
二、变量提升到底是什么?
变量提升,是指 JavaScript 引擎(V8)在执行代码之前,把变量和函数的声明部分 提前到作用域顶部,并给变量赋默认值
undefined。
看一段简化后的模拟代码就清楚了:
ini
// === 你写的代码 ===
console.log(myname);
var myname = '极客时间';
// === JS 引擎眼中的代码(模拟提升后) ===
var myname = undefined; // 声明被"提升"到顶部,默认值是 undefined
console.log(myname); // undefined
myname = '极客时间'; // 赋值留在原地
函数声明的待遇更高------连函数体一起提升:
scss
// === 你写的代码 ===
showName();
function showName() {
console.log('函数被执行了');
}
// === JS 引擎眼中的 ===
function showName() {
console.log('函数被执行了');
}
showName(); // 函数声明整体提升,所以可以先调用,再声明
但注意,函数表达式不享受这个待遇:

javascript
console.log(add); // undefined
var add = function (x, y) { return x + y; };
add 用的是 var 声明,提升的只是变量 add 本身 (值为 undefined),赋值操作(函数体)仍然在原地,所以此时调用 add() 会报错 TypeError: add is not a function。

三、JS 真的是一行一行执行的吗?
如果你真的以为 JS 是老老实实从上到下逐行执行的,变量提升就是你撞上的第一道墙。
总结三条"诡异现象":
| 现象 | 结果 |
|---|---|
| 使用完全未声明的变量 | 报错 ReferenceError ✓ |
在 var 声明之前使用该变量 |
不报错,值是 undefined |
| 在函数声明之前调用该函数 | 不报错,正常执行 |
如果代码真的是一行一行执行的,后面两条根本说不通。
真相是:JavaScript 的执行分为「编译阶段」和「执行阶段」。

JS 是动态脚本语言,没有像 C/Java 那样独立的编译过程,但在代码执行前的"那一刹那",V8 引擎会快速完成一轮编译------生成执行上下文 (Execution Context) 和可执行代码。编译阶段为执行阶段铺好路,执行阶段才真正逐行跑代码。
四、编译阶段到底干了什么?
输入一段代码,编译阶段结束后,V8 吐出两样东西:
- 执行上下文(Execution Context) ------代码运行所需的"环境配置"
- 可执行代码------编好的字节码,供执行阶段使用
执行上下文就是一段代码的"户口本",记录着:
- 变量环境 (Variable Environment) :
var声明的变量和函数声明住在这里 - 词法环境 (Lexical Environment) :
let/const声明的变量住在这里 - this 指向
- 对外部环境的引用(作用域链)
编译阶段做了一件至关重要的事:遍历代码,把所有的声明找出来,在对应的环境中分配好内存空间。
这个过程就是变量提升的物理实现------变量和函数声明的位置没有移动一厘米,只是提前在内存里占好了坑。
所以"变量提升意味着代码被物理移动到顶部"这个说法,严格来说是不准确的。代码原地不动,但内存分配已经提前完成了。
五、变量环境 vs 词法环境------let/const 为什么不一样?
来,再跑一段代码:
ini
console.log(myname); // 变量环境
let myname = '极客时间'; // 词法环境
结果呢?
javascript
ReferenceError: Cannot access 'myname' before initialization
报错了!let 不是也有提升吗?
let 和 const 确实也有提升,但它们和 var 不同流合污。
关键区别:
| var | let / const | |
|---|---|---|
| 住在哪里 | 变量环境 | 词法环境 |
| 提升时赋默认值吗 | ✅ 赋值为 undefined | ❌ 不赋默认值 |
| 声明前访问 | 返回 undefined | 报错(暂时性死区 TDZ) |
let 声明的变量在编译阶段同样分配了内存,但这个内存空间位于词法环境 中。从代码开头到 let 声明之间的区域,变量处于 "暂时性死区" (Temporal Dead Zone, TDZ) ------内存已分配,但你不能碰它。
这就像一个酒店房间:var 的房间提前挂好了门牌号,你可以先进去,里面是空的;let 的房间也挂好了门牌号,但门锁死了,必须等到"入住时间"(声明语句执行)才能进去,否则直接吃闭门羹(ReferenceError)。
javascript
// 暂时性死区示意
// ↓ TDZ 开始
console.log(myname); // 💥 ReferenceError: Cannot access 'myname' before initialization
// ↑ TDZ 结束
let myname = '极客时间';
六、函数提升的"特权"
同样是声明,为什么函数声明比变量声明更"高级"?
因为函数是一等公民(first-class object)。 在编译阶段,函数声明不仅分配了内存,还把整个函数体都存了进去。
arduino
// 编译阶段的函数声明
// 变量环境中:
// showName → 指向函数对象的引用(已包含完整函数体)
所以你在函数声明之前调用它,V8 在变量环境中找到的已经是一个完整的函数对象了,执行起来自然毫无压力。
而 var add = function(){} 这种函数表达式,本质上只是一个 var 变量提升------add 是 undefined,赋值操作要等到执行阶段才发生。
七、完整的编译→执行过程拆解
让我们重新审视开头那段代码,用 V8 的视角完整走一遍:
ini
showName();
console.log(myName);
console.log(add);
var myName = '极客时间';
function showName() {
console.log('函数showName被执行了');
}
var add = function (x, y) {
return x + y;
};
🔧 编译阶段
javascript
变量环境:
myName → undefined (var 声明,分配内存,赋默认值)
add → undefined (var 声明,分配内存,赋默认值)
showName → <function 对象> (函数声明,整体提升)
▶️ 执行阶段(逐行执行)
javascript
第1行:showName()
→ 变量环境中找到 showName 函数对象 → 执行 → 控制台输出 "函数showName被执行了"
第2行:console.log(myName)
→ 变量环境中找到 myName,值为 undefined → 输出 undefined
第3行:console.log(add)
→ 变量环境中找到 add,值为 undefined → 输出 undefined
第4行:myName = '极客时间'
→ 变量环境中 myName 的值从 undefined 更新为 '极客时间'
第5-7行:showName 函数声明 → 编译阶段已处理,执行阶段跳过
第8-10行:add = function(x,y){ return x+y }
→ 变量环境中 add 的值从 undefined 更新为一个函数对象
输出结果完美吻合:
javascript
函数showName被执行了
undefined
undefined

八、面试高频:函数声明 vs 函数表达式
javascript
// ✅ 可以正常工作
foo(); // "hello"
function foo() {
console.log('hello');
}
// ❌ 报错:TypeError: bar is not a function
bar();
var bar = function () {
console.log('world');
};
// 此时 bar 是 undefined,不能当函数调用
本质还是提升的差异------函数声明整体提升,函数表达式只提升了变量名,函数体还在原地。
九、一句话总结
- 变量提升不是物理移动代码,而是编译阶段的内存分配行为
var和函数声明 住在变量环境里,编译后就可用(var→undefined,函数声明 → 完整函数对象)let和const住在词法环境里,编译后内存已分配但被 TDZ 锁住,声明前访问直接报错- JS 不是逐行执行的------编译阶段做"准备工作",执行阶段才真正跑代码
- 函数是一等公民,提升权重最高------声明+定义一起提
写完最大的感受是:变量提升不是什么玄学,它就是 JS 引擎在编译阶段分配内存的一种策略。 理解了执行上下文、变量环境和词法环境的分工,所谓的"提升"不过是一个自然的结果。
正如太阳升起不是太阳在绕着地球转,代码"看起来"被提升到了顶部,也不是代码真的移动了------而是引擎在你看不见的地方,提前做好了准备。