别再背“变量提升”了!深入编译执行,彻底搞懂 JavaScript 运行机制

引言

你是否遇到过这样的代码:在变量声明之前使用它,却得到了 undefined 而不是报错?在函数声明之前调用它,却能正常执行?这背后就是 JavaScript 中著名的 变量提升(Hoisting) 现象。理解它的原理,能帮助我们写出更可预测、更健壮的代码。

本文将结合多个代码示例,深入剖析 JavaScript 的执行过程,涵盖编译阶段与执行阶段、函数优先提升、函数表达式与函数声明的区别,以及 let/constvar 的不同表现。

一、变量提升 ------ 直观现象

1.1 变量提升与函数提升

先来看一段代码:

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

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

var myName = '极客时间';

运行结果:

  • showName() 正常输出 "函数ShowName被执行了"
  • console.log(myName) 输出 undefined,而不是报错

为什么?因为 编译阶段 会先处理变量和函数的声明,将变量声明提升到作用域顶部(初始值为 undefined),函数声明则整体提升。

上述代码在编译后等价于:

javascript 复制代码
// 编译阶段完成的内存分配
var myName = undefined;
function showName() {
    console.log('函数ShowName被执行了');
}

// 执行阶段按顺序执行
showName();
console.log(myName);  // undefined
myName = '极客时间';

1.2 函数是一等对象

在 JavaScript 中,函数是一等对象,可以被赋值给变量,也可以作为参数传递。这也导致了函数声明和函数表达式在提升行为上的差异。

二、编译阶段 vs 执行阶段

JavaScript 代码的执行分为两个阶段:

  1. 编译阶段 :变量和函数声明被处理,变量提升时分配内存并赋值为 undefined,函数声明提升时存储整个函数对象。
  2. 执行阶段:代码按顺序逐行执行,给变量真正赋值,调用函数等。

示例:完整的函数声明与函数表达式

javascript 复制代码
var myName = undefined;
myName = '极客时间';

// 完整的函数声明 ------ 提升整个函数
function foo() {
    console.log('foo');
}

// 函数表达式 ------ 只提升变量 bar,函数体不会提升
var bar = function () {
    console.log('bar');
}

编译后,bar 变量被提升并初始化为 undefined,但右侧的函数表达式只有在执行阶段才会被赋值。

三、函数声明与变量声明的优先级

当同一个作用域内存在同名的函数声明和变量声明时,函数声明会优先于变量声明

来看一个典型例子:

javascript 复制代码
showName();               // 输出 1 还是 2 ?
var showName = function () {
    console.log(2);
}

function showName() {
    console.log(1);
}

实际输出是 1。因为编译阶段函数声明先被提升,随后变量声明(var showName)由于同名而被忽略(不会覆盖已存在的函数)。执行阶段先调用 showName(),输出 1。之后执行到 var showName = function() {...} 时,才对 showName 重新赋值为新函数。

四、多个同名函数声明:后者覆盖前者

如果存在多个同名的函数声明,最终生效的是最后一个。

scss 复制代码
function showName() {
    console.log('极客邦');
}
showName();   // 极客时间

function showName() {
    console.log('极客时间');
}
showName();   // 极客时间

编译时,后面的函数声明会覆盖前面的,因此两次调用都输出 "极客时间"

五、函数表达式不会提升函数体

与函数声明不同,函数表达式只提升变量名,函数体保留在赋值语句中。

javascript 复制代码
showName();   // TypeError: showName is not a function

var showName = function () {
    console.log('函数表达式');
}

编译后等价于:

ini 复制代码
var showName = undefined;
showName();   // 此时 showName 不是函数,报错
showName = function() { ... };

所以,如果希望函数在定义前被调用,必须使用函数声明形式

六、var 的"坑"与 let/const 的改进

很多开发者因为 var 的提升特性而写出难以调试的代码。ES6 引入的 letconst 改善了这一问题。

6.1 let 也提升,但存在暂时性死区(TDZ)

虽然 let 声明的变量也会被提升,但在声明之前访问它会抛出 ReferenceError,这是因为变量被提升但未被初始化,处于"暂时性死区"。

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

对比 var

ini 复制代码
console.log(myName);   // undefined (不会报错)
var myName = '极客时间';

6.2 内存分配的环境不同

  • var 声明的变量属于 变量环境 ,可以在声明前被访问(值为 undefined)。
  • let/const 声明的变量属于 词法环境,在声明前不能访问。

编译阶段两者都会分配内存,但 let/const 的变量在进入作用域时并未初始化,只有执行到声明语句时才会被初始化。

总结

知识点 核心结论
变量提升 var 提升并初始化为 undefinedlet/const 提升但不初始化
函数声明提升 整个函数对象被提升,可在声明前调用
函数表达式提升 只提升变量名,函数体不会提升,调用时需在赋值之后
优先级 函数声明优先于变量声明,同名变量声明被忽略
同名函数 后面的函数声明会覆盖前面的
编译 vs 执行 编译阶段做提升内存分配,执行阶段按顺序运行代码
暂时性死区 (TDZ) letconst 声明前不可访问,否则报错

理解了编译阶段与执行阶段的分离,以及不同声明方式的提升差异,你就能轻松解释诸如"函数提升优先于变量"、"为什么 let 没有变量提升错觉"等问题。希望这篇文章能帮你彻底掌握 JavaScript 的执行原理,写出更加可靠的代码。


本文基于对 JavaScript 引擎底层行为的总结,所有示例均可直接在浏览器控制台或 Node.js 中运行验证。

相关推荐
kyriewen38 分钟前
2026 年了,这 6 个 npm 包可以卸载了——浏览器原生 API 已经能替代
前端·javascript·npm
铁皮饭盒2 小时前
bun直接tsx,优雅!
javascript·后端
_柳青杨4 小时前
一文吃透 Node.js 事件循环:从原理到 Node 20+ 重大变更
javascript·后端
JieE21214 小时前
LeetCode 101. 对称二叉树|JS 递归 + 迭代双解法,彻底搞懂镜像判断
javascript·算法
冬奇Lab16 小时前
AI Workflow 定义的四次演进:从 Markdown 到 JS 脚本,再到分布式多 Agent
javascript·人工智能·agent
一颗烂土豆1 天前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
kyriewen1 天前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
weedsfly1 天前
迭代器、生成器与异步迭代——让数据“按需流动”的艺术
前端·javascript
假如让我当三天老蒯1 天前
前端跨域解决方案(学习用)
前端·javascript·面试
铁皮饭盒1 天前
Bun 哪比 Node.js 快?
javascript·后端