JS 底层执行机制探讨:执行上下文、变量提升与调用栈

JS 代码是怎么跑起来的:执行上下文、变量提升与调用栈


一、从一段"反直觉"的代码说起

javascript 复制代码
showName()
console.log(myname)
var myname = '张三'
function showName() {
    console.log('函数showName执行')
}

第一次看这段代码,直觉是报错------showNamemyname 都在使用之后才声明,怎么可能正常运行?

实际运行结果:

javascript 复制代码
函数showName执行
undefined

showName() 正常调用了,myname 没有报错,输出的是 undefined

这就引出了今天的核心问题:JS 引擎在执行代码之前,到底做了什么?


二、先编译,再执行

JS 不是逐行解释执行的,V8 引擎在真正跑代码之前,会先做一次编译 。这个编译过程发生在执行的前一刻,目的是生成执行上下文(Execution Context)

流程是这样的:

复制代码
读取代码
   ↓
编译阶段(生成执行上下文)
   ↓
执行阶段(从上到下跑代码)

执行上下文可以理解为一个对象,里面装了代码运行需要的所有环境信息:

kotlin 复制代码
执行上下文 {
    变量环境    ← 存 var 声明的变量 和 函数声明
    词法环境    ← 存 let / const 声明的变量
    this        ← this 指向谁
    outer       ← 作用域链,找不到变量往哪找
}

三、变量提升 vs 函数提升

编译阶段做的核心工作,就是提升------把声明整理出来,提前存入执行上下文。

但变量提升和函数提升,行为是不一样的。

3.1 变量提升(var)

var 声明的变量,编译阶段会把变量名 提升,初始值设为 undefined,赋值留在原地等执行阶段处理。

javascript 复制代码
// 开发者写的
var myname = '张三'

// V8 引擎眼里的执行顺序
var myname          // 编译阶段:提升声明,值为 undefined
myname = '张三'    // 执行阶段:赋值才发生

所以 console.log(myname) 在赋值之前执行,拿到的就是 undefined,而不是报错。

3.2 函数提升

函数声明的提升比变量"彻底"得多------整个函数体都会在编译阶段存入变量环境,不只是名字。

javascript 复制代码
// V8 眼里,函数声明被整体提升
function showName() {
    console.log('函数showName执行')
}

// 所以调用在声明之前也没问题
showName()

3.3 函数声明 vs 变量声明,谁优先?

javascript 复制代码
console.log(func)       // 输出函数体,不是 undefined
function func(name) {}
var func = '123'

同一个名字,既有函数声明又有变量声明,编译阶段函数声明优先 覆盖变量声明。func 在变量环境里存的是函数对象,不是 undefined

赋值 func = '123' 发生在执行阶段,所以 console.log 打印出来是函数体。

3.4 let / const:不提升

javascript 复制代码
console.log(b)   // ReferenceError
let b = 9

letconst 声明的变量存在词法环境 里,编译阶段不做提升,也不给初始值。在声明语句执行之前访问,直接报 ReferenceError

这段时间(从进入作用域到声明语句执行)叫做暂时性死区(TDZ,Temporal Dead Zone)

声明方式 存放位置 编译阶段处理 访问未初始化时
var 变量环境 提升,值为 undefined 返回 undefined
函数声明 变量环境 整体提升,值为函数对象 正常访问
let / const 词法环境 不提升,TDZ ReferenceError

四、一道题,把提升规则都用上

javascript 复制代码
var a = 1
function fn(a) {
    var a = 2
    function a() {}
    var b = a
    console.log(a)
}
fn(3)

输出什么?

分析 fn 的编译阶段,V8 生成 fn 的执行上下文:

第一步:处理形参

css 复制代码
变量环境: { a: undefined }

第二步:统一形参和实参

css 复制代码
变量环境: { a: 3 }   ← 实参是 3

第三步:找 var 声明 var a = 2a 已存在,跳过(不覆盖)。

第四步:找函数声明 function a() {} 函数声明优先,覆盖 a

css 复制代码
变量环境: { a: [Function: a] }

进入执行阶段:

javascript 复制代码
var a = 2           // a 被赋值为 2
function a() {}     // 函数声明已在编译阶段处理,跳过
var b = a           // b = 2
console.log(a)      // 2

输出:2

这道题把"形参统一、var 提升、函数提升、执行阶段赋值"全部串联起来了,每个细节都有对应规则。


五、var 的块级作用域问题

javascript 复制代码
function varTest() {
    var x = 1
    if (true) {
        var x = 2        // 同一个 x,不是新的
        console.log(x)   // 2
    }
    console.log(x)       // 2
}

var 不支持块级作用域,if 块里的 var x = 2 修改的是函数作用域里那个唯一的 x,出了 if 块依然是 2

换成 let

javascript 复制代码
function varTest() {
    var x = 1
    if (true) {
        let x = 2        // 独立的块级作用域变量
        console.log(x)   // 2
    }
    console.log(x)       // 1 ← 外层的 x 没被动
}

letx 存在词法环境里,块级作用域结束后销毁,外层的 x 完好无损。


六、调用栈:V8 怎么管理函数调用

6.1 调用栈是什么

调用栈(Call Stack)是 V8 用来追踪当前在执行哪个函数的一种数据结构,后进先出(LIFO)。

每次进入一段可执行代码(全局代码或函数),V8 就会生成一个执行上下文,压入调用栈。函数执行完毕,对应的执行上下文从栈顶弹出销毁。

6.2 全局代码的执行上下文

程序启动时,V8 先创建全局执行上下文压栈:

css 复制代码
调用栈:
┌─────────────────────┐  ← 栈顶
│   全局执行上下文      │
│   变量环境: {        │
│     varTest: fn      │
│   }                  │
│   词法环境: {}       │
└─────────────────────┘

6.3 调用函数时

调用 varTest() 时,V8 编译 varTest 函数体,生成 varTest 的执行上下文,压入栈顶:

css 复制代码
调用栈:
┌─────────────────────┐  ← 栈顶(当前执行这里)
│  varTest 执行上下文  │
│  变量环境: {         │
│    x: undefined      │
│  }                   │
│  词法环境: {}        │
│  outer: 全局上下文   │
├─────────────────────┤
│   全局执行上下文      │
└─────────────────────┘

6.4 函数执行完毕

varTest 执行结束,它的执行上下文从栈顶弹出销毁,控制权回到全局:

makefile 复制代码
调用栈:
┌─────────────────────┐  ← 栈顶
│   全局执行上下文      │
└─────────────────────┘

函数里的局部变量随着执行上下文的销毁一并回收,这就是局部变量生命周期的底层原因。

6.5 编译发生在执行的前一刻

有一个细节值得注意:全局代码的编译在程序启动时完成,函数的编译在被调用的前一刻才发生

函数嵌套越深,调用栈越高。如果递归没有终止条件,调用栈会一直增长,最终触发 Maximum call stack size exceeded(栈溢出)。


七、词法环境与块级作用域的实现

let / const 的块级作用域,在引擎层面也是通过词法环境的栈来实现的。

每进入一个 {} 块,词法环境就会新建一层,存放这个块里的 let / const 变量;块结束,这一层销毁。

javascript 复制代码
function varTest() {
    var x = 1               // → 变量环境
    if (true) {
        let x = 2           // → 词法环境(新建一层)
        // 此时词法环境栈顶有 x = 2
    }
    // if 块结束,词法环境那一层弹出,x = 2 消失
    console.log(x)          // 从变量环境里找,x = 1
}

调用栈管理执行上下文,词法环境内部的栈管理块级作用域,两套机制配合,撑起了 JS 的作用域体系。


八、总结

今天学的东西,核心是搞清楚 JS 代码执行的完整流程:

javascript 复制代码
读取代码
   ↓
编译阶段
├── 创建执行上下文
├── 变量环境:var 声明提升(undefined)、函数声明整体提升
├── 词法环境:let/const 登记但不初始化(TDZ)
└── 确定 this 和 outer(作用域链)
   ↓
执行阶段
├── 代码从上到下顺序执行
├── 赋值在执行阶段发生
└── 函数调用 → 生成新执行上下文 → 压入调用栈
   ↓
函数执行完毕 → 执行上下文弹出销毁

几个容易混的点,整理清楚:

变量提升 ≠ 函数提升 :变量只提升声明,初始值是 undefined;函数声明整体提升,可以在声明之前正常调用。

函数声明优先于变量声明:同名时,编译阶段函数声明会覆盖变量声明。

let/const 有 TDZ:进入作用域到声明语句之间不能访问,这是刻意设计的,让错误提前暴露。

调用栈 = 执行上下文的容器:函数调用压栈,执行完毕弹栈,局部变量随之销毁。

相关推荐
|_⊙5 小时前
Linux 信号
运维·服务器·前端
ZC跨境爬虫5 小时前
跟着 MDN 学 JavaScript day_1:什么是 JavaScript?
开发语言·前端·javascript·ecmascript
广州华水科技5 小时前
单北斗GNSS水库变形监测系统的应用与发展分析
前端
吠品5 小时前
PyTorch 踩坑:libtorch_cpu.so 找不到 iJIT_NotifyEvent 符号
前端·vue.js·elementui
qq_2518364575 小时前
基于java Web 日化商超库存管理系统设计与实现
java·开发语言·前端
xiaofeichaichai5 小时前
Vue 响应式原理
前端·javascript·vue.js
提子拌饭1335 小时前
模态窗鸿蒙PC Electron框架实现技术详解 - 饮料含糖量应用案例分析
前端·javascript·华为·electron·前端框架·开源·鸿蒙
佛山个人技术开发6 小时前
个人建站接单|汽车汽配行业宽屏自适应官网模板 工厂企业定制建站源码
前端·css·前端框架·html·汽车·php
光影少年6 小时前
react的Context 和 Redux 区别?
前端·javascript·react.js·前端框架