别再背“变量提升”了!深入编译执行,彻底搞懂 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 中运行验证。

相关推荐
艾伦野鸽ggg35 分钟前
前端异步请求的状态竞争(请求竞态)问题
前端·javascript·axios
是立不是利39 分钟前
前端交互基石:深入剖析JavaScript三级联动背后的设计哲学
开发语言·前端·javascript
老刘的望远镜1 小时前
AgentScope Java 从零(03):Agent 的记性默认全开,我在第 50 轮翻了车
java·开发语言·javascript
前端精髓1 小时前
React 更新 state 中的对象
javascript·react.js·ecmascript
可乐鸡翅yeah_2 小时前
hls.js 播放质量埋点实战,采集卡顿、起播、错误指标定位线上用户问题
开发语言·前端·javascript·ecmascript·音视频·m3u8·音视频在线播放
Bs_MoneyMagnet4 小时前
基于springboot+vue的宠物殡葬管理平台的设计与实现 源码+文档
java·javascript·vue.js·spring boot·后端·宠物
尾善爱看海5 小时前
Vue 面试进阶篇:Composition API、插槽、自定义指令……8 个章节 + 高频面试题全解析
前端·javascript·vue.js·面试·vue
三小河6 小时前
在 Codex 桌面端接入 DeepSeek 模型(CC Switch 代理中转)
前端·javascript·人工智能
shmily麻瓜小菜鸡6 小时前
JS/TS 易踩坑知识点 — 自动分号插入(ASI)
开发语言·javascript·ecmascript
CoderYanger6 小时前
前端基础——JavaScript(WebAPI)代码案例
java·开发语言·前端·javascript·css·前端框架·html5