《深入理解 JavaScript 执行机制:从 V8 引擎到调用栈》

67 阅读6分钟

JavaScript 执行机制详解:从 V8 引擎到调用栈

JavaScript 作为一门动态、解释型的脚本语言,其执行过程看似简单,实则背后有一套精密的机制在支撑。尤其在现代浏览器中,以 Chrome 的 V8 引擎为代表,对 JavaScript 的编译与执行进行了高度优化。理解 JavaScript 的执行机制,不仅有助于写出更高效的代码,也能避免许多常见的“陷阱”(如变量提升、暂时性死区等)。本文将结合 V8 引擎的工作原理,系统梳理 JavaScript 的执行流程,重点讲解编译阶段执行阶段的区别,以及 varlet/const 在执行上下文中的不同表现。


一、JavaScript 是如何被执行的?

虽然我们常说 JavaScript 是“解释型语言”,但现代 JS 引擎(如 V8)实际上采用了 “即时编译”(JIT, Just-In-Time Compilation) 技术。这意味着:

JavaScript 并非逐行解释执行,而是在执行前会经历一个极快的“编译阶段”,然后再进入执行阶段。

这个过程几乎是“一边编译,一边执行”,但逻辑上可以清晰地划分为两个阶段:

  1. 编译阶段(Compilation Phase)
  2. 执行阶段(Execution Phase)

二、编译阶段:为执行做准备

当一段 JavaScript 代码被 V8 引擎接管时,首先会进入编译阶段。此阶段的核心任务是:

  • 检查语法错误;
  • 进行变量提升(Hoisting);
  • 创建执行上下文(Execution Context);
  • 构建变量环境(Variable Environment)和词法环境(Lexical Environment)。

1. 执行上下文(Execution Context)

执行上下文是 JavaScript 代码运行时的“容器”。每一段可执行代码(全局代码或函数)都会被包裹在一个执行上下文中。V8 使用调用栈(Call Stack)来管理这些上下文。

  • 全局执行上下文:程序启动时首先创建,压入调用栈底部。
  • 函数执行上下文:每次调用函数时创建,压入栈顶;函数执行完毕后出栈并销毁。

2. 变量提升与环境构建

在编译阶段,引擎会扫描代码,识别以下内容:

  • 变量声明varletconst
  • 函数声明
  • 函数参数

然后根据声明类型,分别放入不同的环境:

声明类型存放位置初始值特性说明
var变量环境(VE)undefined允许重复声明,存在变量提升
let / const词法环境(LE)未初始化存在“暂时性死区”(TDZ)
函数声明变量环境(VE)函数引用提升优先级高于 var

📌 关键点letconst 虽然也会被“提升”,但不会被赋值为 undefined,而是处于“暂时性死区”——在声明前访问会抛出 ReferenceError

示例:编译阶段发生了什么?

console.log(a); // undefined
console.log(b); // ReferenceError: Cannot access 'b' before initialization

var a = 1;
let b = 2;
  • 编译阶段:

    • a 被提升到变量环境,值为 undefined
    • b 被记录在词法环境中,但未初始化,处于 TDZ。
  • 执行阶段:

    • 第一行能打印 undefined
    • 第二行试图访问 TDZ 中的 b,报错。

三、执行阶段:真正运行代码

当编译阶段完成后,V8 开始按顺序执行代码。此时:

  • 所有变量和函数已在环境中“就位”;
  • 赋值、函数调用、表达式计算等操作依次进行;
  • 遇到函数调用时,会创建新的函数执行上下文,压入调用栈。

函数调用的完整流程

以如下代码为例:

function fn(a) {
  console.log(a); // 3
  var a = 2;
  console.log(a); // 2
}
fn(3);

实际上被 V8引擎“理解”为

function fn(a) { 
var a; // 变量声明被提升,但此时 a 已经作为参数存在 
console.log(a); // 此时 a 是参数传入的值:3 
a = 2; // 赋值 
console.log(a); // 2 
}
编译阶段(函数 fn 内部):
  1. 创建函数执行上下文;
  2. 参数 a 被放入变量环境,初始值为传入的 3
  3. 发现 var a,但由于 a 已存在(参数),不会重复声明var 允许重复,但此处视为同一标识符);
  4. 函数体代码准备好。
执行阶段:
  1. 执行 console.log(a) → 输出 3
  2. 执行 a = 2 → 修改变量环境中的 a2
  3. 再次输出 a2

💡 注意:函数参数本质上也是 var 级别的声明,因此与 var 共享同一个绑定。


四、varlet/const 的本质区别

虽然三者都用于声明变量,但在执行机制上有根本差异:

特性varlet / const
提升行为提升至变量环境,值为 undefined提升至词法环境,但未初始化
作用域函数作用域块级作用域
重复声明允许不允许
暂时性死区(TDZ)
全局对象属性是(window.a

为什么要有词法环境?

ES6 引入 let/const 和块级作用域后,原有的“变量环境”模型无法满足需求。因此,ECMAScript 规范将执行上下文拆分为:

  • 变量环境(Variable Environment) :存储 var 和函数声明;
  • 词法环境(Lexical Environment) :存储 letconst 和块级绑定。

这种设计使得块级作用域(如 iffor 中的 {})能够独立管理变量生命周期。


五、调用栈:JS 执行的“指挥中心”

V8 引擎使用调用栈来管理函数的执行顺序,其工作方式完全符合“栈”的后进先出(LIFO)原则:

  1. 全局上下文入栈;
  2. 调用函数 → 新上下文入栈;
  3. 函数执行完毕 → 上下文出栈,内存释放;
  4. 栈空 → 程序结束。

调用栈的意义

  • 保证函数执行的隔离性(每个上下文拥有独立的变量环境);
  • 支持递归调用(每次递归都新建上下文);
  • 便于垃圾回收(出栈后,上下文中的变量若无引用,可被回收)。

六、总结:JS 执行机制全景图

  1. JavaScript 并非“纯解释执行” ,而是“编译 + 执行”两阶段模型;
  2. 编译发生在执行前的一瞬间,由 V8 引擎完成;
  3. 执行上下文是代码运行的基本单位,由调用栈管理;
  4. 变量提升的本质是:在编译阶段将声明“注册”到对应环境中;
  5. varlet/const 的差异源于它们被存放在不同的环境(变量环境 vs 词法环境),导致提升行为和作用域规则不同;
  6. 函数调用会创建新上下文,参数和内部声明在编译阶段就被处理好;
  7. 执行完毕即销毁,确保内存高效利用。