在 前端框架/工程化 的构建流程中,代码转译(Transpilation)是一个至关重要的环节。本文将以主流的 JavaScript 转译器 Babel 为例,深入剖析其将 ES6+ 等高级语法转换为兼容性更好代码的底层原理。
概述
编译与转译
首先需要明确两个概念:
- 编译(Compile):通常指将高级语言转换为机器码或字节码,例如 C 语言编译成汇编语言。
- 转译(Transpile):指将一种编程语言的源代码转换为另一种抽象层级相近的源代码。
前端开发中常见的转译操作包括:
- ES6+ -> ES5
- TypeScript -> JavaScript
- Sass/SCSS -> CSS
代码转译示例
Babel 是目前最流行的 JavaScript 转译器。一个简单的转译示例如下:
// ES6 源代码
let foo = 123
// 转译后的 ES5 目标代码
var foo = 123
Babel 转译演示
- 安装核心包
npm install @babel/core @babel/cli @babel/preset-env
- 创建配置文件
babel.config.json
{
"presets": [
"@babel/preset-env"
],
"plugins": []
}
- 执行转译命令
babel src --out-dir dist
- 使用特定插件(例如,将块级作用域声明
let/const 转 var)
{
"presets": [],
"plugins": [
"@babel/plugin-transform-block-scoping"
]
}
转译的原理
核心步骤
Babel 的转译过程可以概括为三个核心步骤,这与编译器的经典架构相似:
- Parse(解析):通过解析器将源代码字符串转换为抽象语法树(Abstract Syntax Tree, AST)。
- Transform(转换):遍历 AST,并调用各种插件对 AST 节点进行增、删、改操作。
- Generate(生成):将转换后的 AST 重新生成为目标代码字符串。

可视化工具
AST Explorer (https://astexplorer.net/) 是一个强大的 AST 可视化工具。它支持多种语言和解析器,可以直观地查看代码与 AST 节点树的对应关系,是理解转译原理的绝佳助手。
Parse详解
“解析”阶段的目标是将源代码转换为结构化的 AST。这个过程通常分为两步:词法分析和语法分析。
词法分析
词法分析(Lexical Analysis)就像阅读文章时先拆分出一个个独立的单词。它会将完整的代码字符串分割成最小的、不可再分的语法单元数组,这些单元被称为 Token。
以代码 let foo = 123 为例,词法分析后可能产生如下 Token 流:
// 源代码
let foo = 123
// 词法分析转换后
const tokens = [
{ "type": { "label": "name" }, "value": "let", "start": 0, "end": 3 },
{ "type": { "label": "name" }, "value": "foo", "start": 4, "end": 7 },
{ "type": { "label": "=" }, "value": "=", "start": 8, "end": 9 },
{ "type": { "label": "num" }, "value": 123, "start": 10, "end": 13 },
{ "type": { "label": "eof" }, "start": 13, "end": 13 }
]
提示:可以在 AST Explorer 等工具中直接查看代码被分割成的 Tokens 集合。
语法分析
语法分析(Syntactic Analysis)的任务是将上一步得到的、扁平的 Token 数组,按照编程语言的语法规则,组织成一棵具有层级结构的 抽象语法树(AST)。这棵树精确地描述了代码的语法构成。

AST 是对源代码的抽象表示,代码中的各种语法结构都有对应的节点类型,例如:
- Literal:字面量(如数字
123,字符串“hello”)
- Identifier:标识符(如变量名
foo)
- Statement:语句
- Declaration:声明语句
- Expression:表达式
- Program:整个程序的根节点
对于 let foo = 123,其简化后的 AST 结构可能如下所示:
const tokens = [
{ type: { label: 'name' }, start: 0, end: 3, value: 'let' },
{ type: { label: 'name' }, start: 4, end: 7, value: 'foo' },
{ type: { label: '=' }, start: 8, end: 9, value: '=' },
{ type: { label: 'num' }, start: 10, end: 13, value: 123 },
{ type: { label: 'eof' }, start: 13, end: 13 }
]
const AST = {
"type": "Program",
"start": 0,
"end": 13,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 13,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 12,
"id": {
"type": "Identifier",
"start": 4,
"end": 7,
"name": "foo"
},
"init": {
"type": "NumericLiteral",
"start": 10,
"end": 13,
"value": 123
}
}
],
"kind": "let"
}
]
}
对于更复杂的嵌套代码(如多层 if 语句),语法分析器需要正确识别其嵌套关系。这通常借助栈(Stack)这种数据结构来实现,确保开括号 { 和闭括号 } 能正确匹配。
转换阶段是 Babel 最核心、最灵活的部分。插件(Plugin)在此阶段发挥作用。
该阶段主要包含两个动作:
- Traverser(遍历器):对 AST 进行深度优先遍历。
- Transformer(转换器):在遍历过程中,当进入或退出某个特定类型的节点时,会调用注册好的“访问者(visitor)”函数,从而对节点进行修改。
下面是一个简化的遍历器实现示例,展示了如何遍历 AST 并触发 visitor:
const AST = {
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "foo"
},
"init": {
"type": "NumericLiteral",
"value": 123
}
}
],
"kind": "let"
}
]
}
function traverser(ast, visitor) {
function traverseArray(array, parent) {
array.forEach((child) => {
traverseNode(child, parent);
});
}
function traverseNode(node, parent) {
if (!node) return;
const path = {
node,
parent,
replaceWith(newNode) {
if (!parent) throw new Error("Cannot replace root node");
for (let key in parent) {
if (Array.isArray(parent[key])) {
const idx = parent[key].indexOf(node);
if (idx > -1) {
parent[key][idx] = newNode;
return;
}
} else if (parent[key] === node) {
parent[key] = newNode;
return;
}
}
},
remove() {
if (!parent) throw new Error("Cannot remove root node");
for (let key in parent) {
if (Array.isArray(parent[key])) {
const idx = parent[key].indexOf(node);
if (idx > -1) {
parent[key].splice(idx, 1);
return;
}
} else if (parent[key] === node) {
parent[key] = null;
return;
}
}
}
};
const visitorFn = visitor[node.type];
if (visitorFn) {
visitorFn(path);
}
switch (node.type) {
case "Program":
traverseArray(node.body, node);
break;
case "VariableDeclaration":
traverseArray(node.declarations, node);
break;
case "VariableDeclarator":
traverseNode(node.id, node);
traverseNode(node.init, node);
break;
case "Identifier":
case "NumericLiteral":
break;
default:
throw new TypeError(`Unknown node type: ${node.type}`);
}
}
traverseNode(ast, null);
}
// 使用遍历器,打印节点信息
traverser(AST, {
VariableDeclaration(path) {
console.log("VariableDeclaration:", path.node.kind);
},
Identifier(path) {
console.log("Identifier:", path.node.name);
}
});
Generate详解
生成阶段的任务很简单:将处理后的 AST 重新转换回字符串形式的代码。这通常通过一个递归函数来实现,该函数根据不同的节点类型拼接字符串。
我们延续上面的例子,在转换阶段将 let 改为 var,将变量名 foo 改为 bar,然后在生成阶段输出最终代码:
const AST = {
// ... AST 结构同上
}
// 使用同一个 traverser 函数进行转换
traverser(AST, {
VariableDeclaration(path) {
path.node.kind = 'var'
},
Identifier(path) {
path.node.name = 'bar'
}
});
// 生成器:递归地将 AST 转换为代码字符串
function generator(node) {
switch (node.type) {
case "Program":
return node.body.map(generator).join("\n");
case "VariableDeclaration":
return (
node.kind +
" " +
node.declarations.map(generator).join(", ") +
";"
);
case "VariableDeclarator":
return generator(node.id) + " = " + generator(node.init);
case "Identifier":
return node.name;
case "NumericLiteral":
return node.value;
default:
throw new TypeError("Unknown node type: " + node.type);
}
}
const ret = generator(AST)
console.log(ret) // 输出:var bar = 123;
// 最后将这个字符串写入到目标文件即可
自定义Babel插件
理解了上述原理后,编写一个 Babel 插件就变得非常直观。插件本质上就是一个返回 visitor 对象的模块。例如,实现上面演示功能的插件如下:
// my-plugin.js
module.exports = ({ types: t }) => {
return {
name: "myPlugin",
visitor: {
VariableDeclaration(path) {
path.node.kind = "var";
},
Identifier(path) {
path.node.name = "bar";
},
},
};
};
将其配置到 Babel 的 plugins 中即可生效。这正是在 HTML/CSS/JS 开发中,开发者能够通过插件机制灵活处理各种语法转换和代码优化的基础。
本文深入解析了 Babel 的转译原理,从源代码到 AST 的解析,再到遍历修改和最终代码生成。理解这套流程,不仅能帮助开发者更好地使用和配置 Babel,也为理解其他编译/转译工具(如 ESLint、Prettier、代码压缩工具)的工作原理奠定了基础。更多关于前端工程化与编译原理的深入探讨,欢迎在云栈社区交流分享。