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

相关推荐
用户852495071841 小时前
为什么变量能 未定义先使用?
javascript·程序员
Larcher2 小时前
JS 变量提升:代码没动,为什么执行顺序就变了?
前端·javascript·前端框架
就叫_这个吧3 小时前
JavaScript中常用事件示例展示附源码
开发语言·javascript·html
代码N年归来仍是新手村成员3 小时前
【AWS】Lambda 初识与服务部署
javascript·react.js·ai·node.js·云计算·ai编程·aws
云水一下3 小时前
JavaScript 从零基础到精通系列:流程控制、函数与作用域
前端·javascript
丷丩3 小时前
MapLibre GL JS第28课:PMTiles源和协议
javascript·gis·map·mapbox·maplibre gl js
之歆4 小时前
Day24_JavaScript正则表达式与性能优化实战:从入门到精通
javascript·性能优化·正则表达式
柚子科技4 小时前
Vue3 响应式原理:我被 ref 和 reactive 坑了3次后终于搞懂了
前端·javascript·vue.js
五月君_4 小时前
继 React、Vue 之后,Three.js 也有 Skills 了!AI 写 3D 终于不“晕”了
javascript·vue.js·人工智能·react.js·3d