JS 代码执行原理
约 3202 字大约 11 分钟
2025-11-06
相关信息
V8 是由 Google 开发的一个开源 JavaScript 引擎,最初用于 Chrome 浏览器,但现在也广泛应用于其他环境中,包括 Node.js。
V8 引擎的设计目标是高性能执行 JavaScript 代码,支持 JIT(即时编译)技术,将 JavaScript 编译为高效的机器码来提高执行速度。
V8 是一个开源项目,仓库地址
概述
JavaScript 代码是由 JavaScript 引擎 执行的。
引擎的任务是将你编写的 JavaScript 代码转化为计算机能够理解并执行的机器码。
在了解 JavaScript 引擎之前,我们首先要明白 V8 引擎,虽然 V8 并不是唯一的 JavaScript 引擎,但它是最常用且最具代表性的引擎之一。
常见的 JavaScript 引擎有:
- V8:由 Google 开发,主要用于 Chrome 和 Node.js。
- SpiderMonkey:由 Mozilla 开发,主要用于 Firefox。
- JavaScriptCore (JSC):由 Apple 开发,主要用于 Safari。
- Chakra:由 Microsoft 开发,曾用于旧版 Edge 浏览器。
我们将重点以 V8 引擎为例,来学习 JavaScript 代码的执行过程。
执行过程
JavaScript 引擎执行代码通常分为以下几个步骤:
- 解析(Parsing)
- 字节码生成(Bytecode Generation)
- 即时编译 (JIT Compilation)
- 执行(Execution)
- 垃圾回收(Garbage Collection)
解析(Parsing)
解析是 JavaScript 引擎处理代码的第一步。它的任务是将原始的 JavaScript 源代码转换成一个更为结构化的表示形式,这通常是 抽象语法树(AST)。
- 抽象语法树(AST):它是代码的树状结构,表示代码中的各种语法成分(如变量声明、函数调用、运算符等)及它们之间的关系。AST 帮助引擎理解代码的逻辑和结构。
假设你写了以下代码:
const i = 1;
console.log(i);引擎会将其解析成以下抽象语法树(AST):
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "i"
},
"init": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
],
"kind": "const"
},
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"computed": false,
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "log"
}
},
"arguments": [
{
"type": "Identifier",
"name": "i"
}
]
}
}
]
}通过解析,JavaScript 引擎会将代码变成结构化的 AST,方便后续的处理。
字节码生成(Bytecode Generation)
字节码是介于源代码和机器码之间的一种中间代码,它是一种低级语言指令集,JavaScript 引擎会将 AST 转换成字节码。字节码并不直接是机器码,它需要经过进一步的编译才能执行。
字节码的生成步骤由一个 字节码生成器 完成,伪代码如下:
Bytecode* generateBytecode(AST* ast) {
BytecodeGenerator generator(ast);
return generator.generate();
}假设我们有一个简单的 AST,字节码可能会类似于:
CONST i- 声明常量iLOAD_CONST 1- 加载常量1到栈STORE i- 将栈顶的值存储到变量iLOAD_GLOBAL "console"- 加载全局对象consoleLOAD_PROPERTY "log"- 加载log方法LOAD i- 加载变量i的值CALL_METHOD 1- 调用log方法,传递一个参数
这些字节码指令是执行的基础,解释器会逐条执行这些指令。
即时编译 (JIT Compilation)
即时编译(JIT,Just-In-Time Compilation) 是现代 JavaScript 引擎的核心优化技术之一。它允许引擎在代码执行时对某些频繁执行的部分进行优化,并将其编译成机器码,提高性能。
JIT 编译通常分为两个阶段:
- 基线编译 (Baseline JIT):快速编译字节码成机器码,尽量减少编译时间。
- 优化编译 (Optimized JIT):对热点代码(即频繁执行的代码部分)进行深度优化,使用复杂的优化策略(如内联函数、循环优化等)。
通过 JIT,代码不再是单纯的字节码,而是转换为更高效的机器码,使得后续执行更加快速。
执行(Execution)
在 JavaScript 引擎中,代码的执行是通过 解释执行 和 JIT 编译 混合进行的。引擎在启动时会选择先解释执行字节码,随着代码执行次数的增加,将热点代码提升到 JIT 编译,最终生成机器码加速执行。
执行过程示例:
- 解释执行:引擎开始时使用解释器逐行执行字节码。
- 基线 JIT 编译:对于多次执行的代码,基线 JIT 编译会将字节码编译成机器码。
- 优化 JIT 编译:对于最频繁执行的代码,优化 JIT 会进行更深入的优化,提升执行效率。
垃圾回收(Garbage Collection)
垃圾回收(GC)是 JavaScript 引擎用来管理内存的机制。JavaScript 使用自动垃圾回收来避免内存泄漏和过多的手动内存管理。垃圾回收的基本策略通常有:
- 标记-清除(Mark-and-Sweep):标记不再使用的对象并清理它们。
- 分代收集(Generational Garbage Collection):将对象分为“年轻代”和“老年代”,并根据对象的生命周期来决定回收策略。
垃圾回收是 JS 引擎维护内存安全的关键部分,通常它会在后台自动进行,不影响应用的执行。
总结
JavaScript 代码的执行流程从解析开始,将代码转换为 AST,生成字节码,再通过 JIT 编译生成机器码。通过解释执行、JIT 编译和垃圾回收等机制,JavaScript 引擎优化代码执行,确保性能和内存管理。希望这些笔记能够帮助你从一个小白的角度理解 JavaScript 执行原理,并为后续的学习奠定基础。
AST
什么是 AST(抽象语法树)?
AST(Abstract Syntax Tree) 是将 JavaScript 源代码解析成的一种树状结构,表示代码的语法结构和层级关系。
在 AST 中,每一个节点表示代码的一个语法成分(如变量声明、函数调用、运算表达式等)。
JavaScript 引擎在执行时,也会先将代码解析成 AST,然后生成字节码或机器码。而在 构建工具 中,AST 用于静态分析和代码转换。
const a = 1;
function sum(x, y) { return x + y; }解析成 AST 后,可能类似:
Program
├─ VariableDeclaration (const a = 1)
└─ FunctionDeclaration (sum)
├─ Identifier: x
├─ Identifier: y
└─ ReturnStatement
└─ BinaryExpression (x + y)Node 版本的 AST 转换器
在 Node.js 环境里,构建工具会使用 AST 来处理代码,有几个常用库:
- Acorn:轻量级 JS 解析器,将 JS 代码解析成 AST。
- Esprima:功能强大的 JS 解析器,也生成 AST。
- Babel Parser:Babel 内部使用的解析器,支持最新的 JavaScript/TypeScript 语法,生成 AST。
- Recast:可以在 AST 上修改代码,然后生成新的 JS 代码,常用于代码转换和自动化重构。
Tree Shaking(摇树优化)原理
Tree Shaking 是现代打包工具(Webpack、Rollup、Vite 等)用来去掉没用的代码的技术,其核心是 静态分析 AST:
解析代码
将每个模块的源代码转换成 AST。
分析导入导出
AST 中有每个 import 和 export 的信息,打包工具会分析哪些变量和函数实际被使用。
标记未使用代码
对没有被引用的函数、变量或模块进行标记,这些就是“垃圾代码”。
生成新代码
将未使用的代码移除,然后生成最终打包文件。
示例:
原始代码:
export function foo() { console.log('foo'); }
export function bar() { console.log('bar'); }
foo();Tree Shaking 后只保留:
function foo() { console.log('foo'); }
foo();bar 被移除了,因为在任何地方都没有被调用。
和 JS 引擎的关系
- JS 引擎:解析 AST 用于执行代码,然后生成字节码、机器码。
- 打包工具:解析 AST 用于分析和优化代码,最终生成更小、更高效的打包文件。
- 相同点:都是解析源代码生成 AST。
- 不同点:一个是动态执行(Runtime),一个是静态分析(Build Time)。
总结
- AST 是代码静态分析和优化的基础。
- Node 环境有很多 AST 解析器(Acorn、Esprima、Babel Parser)可用。
- Tree Shaking 就是通过 AST 找出未使用代码并删除,实现“去除垃圾代码”。
- 你可以把它想成:JS 引擎在运行时也会做类似分析,只是它是为了执行,而 Tree Shaking 是为了打包优化。
AST的原理
AST(抽象语法树)是解析器(Parser)根据源代码生成的树状结构,它本质上是 把代码的语法结构抽象出来,不是简单的字符串匹配。
解析器的流程大致可以分为两个阶段:
词法分析(Lexical Analysis / Tokenization)
这一阶段的任务是把一段代码拆成最小的单位,叫做 Token(标记)。
Token 可以是:
- 关键字 (
const,function) - 标识符 (
i,foo) - 操作符 (
+,=) - 字符串/数字常量 (
"hello",123) - 分隔符 (
;,{,})
每个 Token 会有类型(type)和内容(value)。
例子:
const i = 1;词法分析后可能得到:
[
{ type: "Keyword", value: "const" },
{ type: "Identifier", value: "i" },
{ type: "Operator", value: "=" },
{ type: "NumericLiteral", value: 1 },
{ type: "Punctuation", value: ";" }
]注意:这一步和正则有一点相似,底层确实会用模式匹配,但它比你想的逐字符匹配复杂很多。它会考虑上下文来判断一个字符序列是什么类型。
语法分析(Syntax Analysis / Parsing)
语法分析会把 Token 串组合成树状结构,形成 AST。
树的每个节点表示一段语法,例如:
- VariableDeclaration(变量声明)
- FunctionDeclaration(函数声明)
- BinaryExpression(加法、减法等运算)
节点之间用 父子关系 表示嵌套结构和执行顺序。
例子:
const i = 1;AST:
VariableDeclaration
├─ kind: const
└─ declarations
└─ VariableDeclarator
├─ id: Identifier (i)
└─ init: NumericLiteral (1)解析器是如何区分不同类型的 Token 的?
不是用一个超级大正则去匹配整个文件,而是 通过状态机 + 语法规则(Grammar) 来分析。
核心概念:
上下文敏感:比如 const i = 1;,解析器看到 const 就知道后面应该是标识符,然后可能是 =,再是表达式。
递归下降(Recursive Descent Parser):
- 最常见的解析器算法。
- 每个语法规则对应一个函数,函数内部会根据 Token 类型递归解析子结构。
优先级和嵌套:
- 运算符、括号、函数体等都通过递归来处理。
- 括号嵌套、对象字面量嵌套、函数嵌套都可以通过递归结构正确生成 AST。
伪代码示意:
function parseVariableDeclaration(tokens) {
if (tokens.peek().type === "Keyword" && tokens.peek().value === "const") {
tokens.next(); // consume "const"
let id = parseIdentifier(tokens); // 解析变量名
let init = null;
if (tokens.peek().value === "=") {
tokens.next(); // consume "="
init = parseExpression(tokens); // 解析右侧表达式
}
return {
type: "VariableDeclaration",
kind: "const",
declarations: [{ id, init }]
};
}
}发现:解析器是通过规则和函数递归来匹配不同的语法,而不是简单的正则。
总结原理
- AST 的生成 不是正则一行行匹配,而是:
- 词法分析:拆成 Token。
- 语法分析:用递归函数 + 状态机 + 语法规则把 Token 转成树。
- 解析器理解上下文和嵌套,能够区分:
- 值(1、"hello"、true)
- 关键字(const、function)
- 语法结构(函数体、循环体、对象字面量)
- 树状结构方便后续:
- 生成字节码或机器码(JS 引擎)
- 静态分析、Tree Shaking、代码转换(Babel/Rollup/Webpack)
记录
AST 的底层原理很复杂,包括词法分析、递归下降解析、状态机、优先级处理、嵌套解析……自己实现一个完整解析器几乎不可能在短时间内搞定,而且还容易出错。
几乎所有现代高级语言在编译或执行之前都会用 AST(抽象语法树) 作为中间表示,无论是 解释型语言(JavaScript、Python)还是 编译型语言(C、Java、Go、Rust)。只是用途和处理方式略有不同。
解释型语言
以 JavaScript、Python 为例:
- JS 引擎(如 V8)流程:
- 源码 → 词法分析 → Token
- Token → 语法分析 → AST
- AST → 字节码生成 → 执行(解释或 JIT 编译)
- 执行过程中可能优化(JIT)、回收内存(GC)
- AST 在这里主要是运行时的中间表示,供引擎分析和执行。
编译型语言
以 C、C++、Java、Go、Rust 为例:
- 编译流程大致相同:
- 源码 → 词法分析 → Token
- Token → 语法分析 → AST
- AST → 中间表示(IR, Intermediate Representation)
- LLVM IR(C、C++、Rust)
- Java 字节码(JVM)
- Go SSA(Single Static Assignment)
- IR → 优化、生成目标机器码
- 最终生成可执行文件或字节码(JVM/Go/Rust/C)
- AST 在编译器中作用:
- 捕捉语法结构
- 做语义分析(类型检查、作用域、符号表)
- 生成中间表示,用于优化和代码生成
AST 是现代语言不可或缺的核心,它是“语言和机器之间的桥梁”,无论是运行时执行还是编译成机器码,都离不开它。