拆穿 JavaScript 变量提升的"魔术"——从一段反直觉代码说起

拆穿 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 吐出两样东西:

  1. 执行上下文(Execution Context) ------代码运行所需的"环境配置"
  2. 可执行代码------编好的字节码,供执行阶段使用

执行上下文就是一段代码的"户口本",记录着:

  • 变量环境 (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 不是也有提升吗?

letconst 确实也有提升,但它们和 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 变量提升------addundefined,赋值操作要等到执行阶段才发生。


七、完整的编译→执行过程拆解

让我们重新审视开头那段代码,用 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,不能当函数调用

本质还是提升的差异------函数声明整体提升,函数表达式只提升了变量名,函数体还在原地。


九、一句话总结

  1. 变量提升不是物理移动代码,而是编译阶段的内存分配行为
  2. var 和函数声明 住在变量环境里,编译后就可用(varundefined,函数声明 → 完整函数对象)
  3. letconst 住在词法环境里,编译后内存已分配但被 TDZ 锁住,声明前访问直接报错
  4. JS 不是逐行执行的------编译阶段做"准备工作",执行阶段才真正跑代码
  5. 函数是一等公民,提升权重最高------声明+定义一起提

写完最大的感受是:变量提升不是什么玄学,它就是 JS 引擎在编译阶段分配内存的一种策略。 理解了执行上下文、变量环境和词法环境的分工,所谓的"提升"不过是一个自然的结果。

正如太阳升起不是太阳在绕着地球转,代码"看起来"被提升到了顶部,也不是代码真的移动了------而是引擎在你看不见的地方,提前做好了准备。

相关推荐
东风破_1 小时前
一文搞懂 JavaScript 变量声明:var、let、const 到底有什么区别?
前端·javascript
月光刺眼1 小时前
🎶二分 · 双指针 · 滑动窗口 · 螺旋矩阵:数组算法四题拆解
javascript·算法
光影少年2 小时前
Redux Toolkit 用法、解决原生Redux 冗余问题
开发语言·前端·javascript·react.js·中间件·前端框架·ecmascript
云水一下2 小时前
JavaScript 从零基础到精通系列:DOM 操作与事件驱动编程
前端·javascript
零陵上将军_xdr2 小时前
后端转全栈学习-Day3-JavaScript 基础-1
开发语言·javascript·学习
GISHUB2 小时前
Express + TypeScript + ESM 后端服务搭建教程
javascript·typescript·express
ZC跨境爬虫2 小时前
跟着 MDN 学CSS day_32:(Web字体深度解析与实践指南)
前端·javascript·css·ui·html
sugar__salt2 小时前
JavaScript 数组去重全解:6 种核心方法
javascript
SEO_juper2 小时前
JavaScript 渲染:AI 智能体无法读取,直接影响收录
开发语言·前端·javascript·aigc·seo·跨境电商·geo