吃透 JS 执行机制:从编译到执行,V8 引擎到底做了什么?

36 阅读8分钟

吃透 JS 执行机制:从编译到执行,V8 引擎到底做了什么?

作为前端开发者,你一定遇到过这些困惑:为什么console.log(a)在var a = 1前执行不会报错,只是输出undefined?为什么let声明的变量提前访问就会报错?函数声明为什么总能优先于变量被访问? 这些问题的本质,都指向 JS 的核心执行机制 ——V8 引擎的编译阶段和执行阶段。本文会从底层逻辑出发,结合 V8 引擎的工作流程、执行上下文、调用栈等核心概念,把 JS 执行机制讲得明明白白,新手也能轻松理解。

一、JS 执行的核心:不是 “边写边执行”,而是 “先编译后执行”

很多人误以为 JS 作为 “脚本语言” 是逐行执行的,但实际上,V8 引擎在执行 JS 代码前,会先经历编译阶段,再进入执行阶段—— 这也是 JS 执行机制的核心。

1.1 先明确两个核心角色

  • V8 引擎:Chrome/Node.js 的 JS 执行内核,负责 JS 代码的编译和执行;
  • 执行流程:代码编写顺序 ≠ 执行顺序,编译阶段会提前处理变量、函数声明,为执行阶段做准备。

1.2 JS 执行的两个核心阶段

阶段核心工作执行时机
编译阶段检测语法错误、变量 / 函数提升、创建执行上下文、初始化变量环境 / 词法环境代码执行前的 “一霎那”
执行阶段按顺序执行代码、给变量赋值、执行函数调用、销毁执行上下文(垃圾回收)编译完成后,逐行执行

关键区别:JS 的编译不是像 Java/C++ 那样 “一次性编译成机器码”,而是边编译、边执行(准确说是 “编译

二、V8 引擎的核心设计:执行上下文与调用栈

要理解编译 / 执行阶段,必须先掌握 “执行上下文” 和 “调用栈”—— 这是 V8 管理 JS 执行的两大核心机制。

2.1 执行上下文:代码的 “运行环境容器”

一段可执行的 JS 代码(全局代码 / 函数代码),会被 V8 包裹成一个执行上下文对象,这个对象包含代码执行所需的所有信息:

  • 变量环境(Variable Environment):存储var声明的变量、函数声明、参数;
  • 词法环境(Lexical Environment):存储let/const声明的变量(暂时性死区的核心载体);
  • this指向、作用域链、可执行代码等。

执行上下文分为两类:

  • 全局执行上下文:程序启动时创建,整个程序只有 1 个,直到页面关闭 / 进程结束才销毁;
  • 函数执行上下文:每次调用函数时创建,函数执行完毕后销毁(局部变量随之回收)。

2.2 调用栈:执行上下文的 “管理者”

V8 用调用栈(Call Stack) 来管理执行上下文的入栈、执行、出栈,遵循 “先进后出” 的栈规则:

    1. 程序启动,全局执行上下文先被压入调用栈(栈底,永不提前出栈);
    1. 遇到函数调用,创建对应的函数执行上下文,压入调用栈(栈顶);
    1. 栈顶的执行上下文优先执行,执行完毕后出栈,垃圾回收机制回收局部变量;
    1. 所有代码执行完毕,只剩全局执行上下文在栈底。

调用栈执行流程示例:

var a = 1; 
function fn() { 
   console.log(a); 
}
fn(); // 函数调用
  • 第一步:全局执行上下文入栈(栈底);
  • 第二步:调用fn(),fn的函数执行上下文入栈(栈顶);
  • 第三步:执行fn的代码,输出1,执行完毕后fn的上下文出栈;
  • 第四步:全局代码执行完毕,等待页面关闭后全局上下文出栈。

三、编译阶段:V8 到底做了哪些准备工作?

编译阶段是 “变量提升”“暂时性死区” 的核心原因,我们以一段代码为例,拆解编译阶段的完整流程:

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

3.1 全局代码的编译阶段

全局代码编译时,V8 会创建全局执行上下文对象,并完成以下工作:

  1. 初始化变量环境: ◦ 扫描var声明:a被提升,初始值为undefined; ◦ 扫描函数声明:fn被完整提升,值为函数体(函数声明优先级 > 变量声明); ◦ 绑定this:全局this指向window(浏览器)/global(Node)。
  2. 初始化词法环境:全局无let/const,词法环境为空;
  3. 检测语法错误:若有语法错误(如少分号、括号不匹配),直接终止执行。 此时全局执行上下文的变量环境:
全局变量环境 = {
a: undefined,
fn: [Function: fn] 
}

3.2 函数代码的编译阶段(调用fn(3)时)

调用fn(3)时,V8 会立即编译fn的函数代码,创建函数执行上下文对象,步骤如下:

  1. 处理形参和 arguments

    • 形参a绑定实参值3
    • 创建arguments对象:arguments = [3]
  2. 初始化变量环境

    • 扫描var声明:var a仅做声明(不覆盖形参a3),var b初始值为undefined
    • 若有函数声明(如function a() {}),则函数声明优先级最高,会覆盖形参 / 变量(后文示例)。
  3. 初始化词法环境:函数内无let/const,词法环境为空;

  4. 绑定作用域链:函数上下文的作用域链 = 自身变量环境 → 全局变量环境。

此时函数执行上下文的变量环境:

fn变量环境 = {
  a: 3, // 形参赋值优先于var声明 
  b: undefined
}

3.3 变量提升的优先级规则

编译阶段的 “提升优先级” 是核心,记住这个顺序:函数声明 > 函数参数 > var 变量声明 示例验证(含函数声明):

var a = 1; 
function fn(a) { 
  console.log(a); // 输出:[Function: a] 
  var a = 2; 
  function a() {}; // 函数声明 
}
fn(3);

编译fn时,变量环境初始化顺序:

  1. 形参a赋值为3;
  2. 函数声明function a() {}覆盖形参a,a变为函数体;
  3. var a仅声明,不改变已有值; 因此第一个console.log(a)输出函数体,而非3。

四、执行阶段:按顺序赋值与执行

编译阶段完成后,V8 进入执行阶段,核心工作是 “给变量赋值” 和 “执行代码逻辑”—— 此时才是真正的 “逐行执行”。

4.1 全局代码的执行阶段

还是以之前的代码为例:

var a = 1; // 编译阶段a已提升为undefined 
function fn(a) { /* 函数体 */ } 
fn(3);

执行阶段流程:

  1. 执行var a = 1:将全局变量环境中a的值从undefined改为1;
  2. 执行function fn(...):函数声明已在编译阶段完成,无额外操作;
  3. 执行fn(3):触发fn的函数执行上下文创建(编译)→ 执行fn的代码。

4.2 函数代码的执行阶段

以fn(3)为例,编译阶段已准备好变量环境,执行阶段逐行处理:

function fn(a) { 
  console.log(a); // 编译后a=3 → 输出3
  var a = 2;   // 赋值:将变量环境中a的值从3改为2 
  var b = a;   // 赋值:将b的值从undefined改为2 
}

执行阶段的核心特点:

  • 仅处理 “赋值操作”,不处理 “声明操作”(声明已在编译阶段完成);
  • 变量查找遵循 “作用域链”:先找自身变量环境,找不到再找全局。

五、var vs let/const:编译阶段的核心区别

varlet/const的差异,本质是编译阶段 “存储位置” 和 “初始化规则” 不同:

特性varlet/const
存储位置变量环境词法环境
初始化时机编译阶段初始化为 undefined编译阶段未初始化(暂时性死区)
提升特性完全提升(可提前访问,值为 undefined)部分提升(提前访问报错)
重复声明允许不允许

5.1 暂时性死区(TDZ):let/const 的核心特性

let/const声明的变量存储在词法环境中,编译阶段仅 “创建绑定”(登记变量名),但不初始化 —— 此时变量处于 “暂时性死区”,提前访问会报错:

console.log(b); // 报错:Cannot access 'b' before initialization 
let b = 4; // 执行到这一行,b才脱离死区,完成初始化

5.2 示例对比:var vs let

// var:编译阶段初始化undefined,可提前访问
console.log(a); // undefined 
var a = 1;

// let:编译阶段未初始化,提前访问报错 
console.log(b); // ReferenceError 
let b = 2;

六、JS 执行机制的核心总结

  1. 执行规则:JS 不是纯逐行执行,而是 “一段代码先编译,后执行”—— 编译在执行前的一霎那完成,编译阶段做准备,执行阶段做赋值;
  2. 核心载体:执行上下文是代码的运行环境,包含变量环境 / 词法环境;调用栈管理执行上下文的入栈、执行、出栈;
  3. 提升规则:编译阶段的变量 / 函数提升有优先级(函数声明 > 参数 > var),let/const 仅部分提升(存在暂时性死区);
  4. 垃圾回收:函数执行上下文出栈时,局部变量会被垃圾回收,全局变量需手动清理或页面关闭后回收。