Babel 是怎样把新语法转成旧语法的?语法转换和 polyfill 有什么区别?
Babel 先把源码解析成 AST,再由插件遍历 AST 改写节点,最后生成新代码。插件按配置顺序执行,preset 按相反顺序执行。箭头函数、可选链这类语法可以改写,Promise、Array.prototype.includes 这类 API 要靠 core-js 等 polyfill 补上。
简答
项目里写了
const add = (a, b) => a + b和user?.name,要在老浏览器上运行,就得把它们改写成老浏览器认识的写法,这是 Babel 做的事。它的工作分三步:解析,把源码变成一棵抽象语法树,也就是 AST;转换,让插件遍历这棵树、修改节点;生成,把改完的树重新输出成代码,同时可以生成 Source Map。转换全靠插件,每个插件只管一种语法,比如箭头函数插件只把箭头函数节点换成普通函数节点。插件太多不好配,所以有了 preset,它是一组插件的集合,最常用的是
@babel/preset-env,按你配置的目标浏览器自动挑出需要的插件。顺序上,插件按配置从前往后执行,preset 则从后往前执行。要分清的是语法和 API。箭头函数、可选链、class 是语法,Babel 能把它们改写成等价的旧代码;
Promise、Array.prototype.includes、Object.fromEntries是运行时才有的对象和方法,改写代码变不出来,需要用 polyfill 在运行时补上实现,通常用 core-js。preset-env的useBuiltIns: "usage"能按代码里实际用到的 API 自动引入对应的 polyfill。要注意,Babel 默认只转换语法,不会自动补 API,也不做类型检查。用 Babel 编译 TypeScript 时,它只是把类型注解删掉,类型错误要另外用
tsc检查。
一段代码经过了什么
以箭头函数为例:
// 输入
const add = (a, b) => a + b;
// 输出(目标浏览器不支持箭头函数时)
var add = function add(a, b) {
return a + b;
};
解析阶段,@babel/parser 把这行代码变成 AST,里面有一个 VariableDeclaration 节点,它的初始值是一个 ArrowFunctionExpression 节点。转换阶段,箭头函数插件的访问器遇到 ArrowFunctionExpression 就把它替换成 FunctionExpression,表达式体改成带 return 的语句块;另一个插件把 const 改成 var。生成阶段,@babel/generator 把新的树打印成代码。
为什么不用字符串替换,而要先转成 AST?因为代码的结构靠字符串很难判断,比如 => 可能出现在字符串或注释里,箭头函数里的 this 还要特殊处理。AST 上每个节点都知道自己是什么,插件只需关心它要改的那种节点。
写一个最简单的插件
插件本质上是一个返回 visitor 对象的函数,visitor 的键是要处理的节点类型:
export default function removeConsole({ types: t }) {
return {
visitor: {
CallExpression(path) {
const callee = path.get('callee');
if (callee.isMemberExpression() && t.isIdentifier(callee.node.object, { name: 'console' })) {
path.remove();
}
},
},
};
}
Babel 遍历 AST 时,每遇到一个函数调用节点就调用 CallExpression。如果调用的是 console.xxx(...),就把这个节点删掉。生产构建里去掉 console 的插件就是这个思路。
只转语法为什么还会报错
假设代码里用了 [1, 2].includes(2),目标是一个不支持 includes 的老浏览器。Babel 转换后代码原封不动,因为 includes 只是一次普通的方法调用,语法上没有新东西。运行时老浏览器的数组原型上找不到 includes,就报 is not a function。
这类问题要靠 polyfill:在代码运行前,先给 Array.prototype 补上一个 includes 实现。用 preset-env 配合 useBuiltIns: "usage" 和 core-js,Babel 扫描到代码里用了 includes,就自动在文件开头插入对应的 core-js 引入。目标浏览器怎么配、polyfill 怎么取舍,见《编译目标、polyfill 和浏览器兼容性是什么关系?》。
常见追问
Babel 和 webpack 是什么关系?
webpack 负责从入口出发找出所有模块并打包,它自己只认识 JavaScript 和 JSON。遇到需要转换的文件,就交给
babel-loader,由 Babel 转换完再交回 webpack 继续打包。
现在还需要 Babel 吗?
很多场景已经被更快的工具替代了,比如 Vite 开发时用 esbuild 转换,也有项目用 SWC。可是需要自定义 AST 转换时,Babel 的插件生态仍然最丰富,React Compiler 等工具也以 Babel 插件的形式提供。
polyfill 会污染全局吗?
会。普通的 core-js 引入会直接修改
Array.prototype这些全局对象,应用项目里一般没问题。写给别人用的库最好不要污染全局,可以用@babel/plugin-transform-runtime配合corejs选项,把 API 调用改写成从模块里导入的实现。