怎么理解AST,并实现手写babel插件
程序表面看就是文本文件里的字符,计算机首先对其进行词法、语法分析,然后用某种计算机能理解的低级语言来重新表达程序,而这其中AST就是一个重点。
水平有限,粗浅的说下AST的理解,并通过手写babel插件来加深理解。
先看看, JS 是怎么编译执行的
- 词法分析,将原始代码生成
Tokens
- 语法分析,将
Tokens
生成抽象语法树(Abstract Syntax Tree,AST) - 预编译,当 JavaScript 引擎解析脚本时,它会在预编译期对所有声明的变量和函数进行处理!并且是先预声明变量,再预定义函数!
- 解释执行,在执行过程中,JavaScript 引擎是严格按着作用域机制(scope)来执行的,并且 JavaScript 的变量和普通函数作用域是在定义时决定的,而不是执行时决定的。
词法分析
词法分析:将原始代码转化成最小单元的词语数组,最小单元的词语数组的专业名词是Tokens
,这里注意,词语会加上相应的类型。
比如:var a = 'hello'
词法分析之后输出Tokens
如下:
[ { type: "Keyword", value: "var", }, { type: "Identifier", value: "a", }, { type: "Punctuator", value: "=", }, { type: "String", value: "'hello'", }, ];
可借助网站esprima在线生成。
语法分析
语法分析:将Tokens
按照语法规则,输出抽象语法树
,抽象语法树其实就是JSON对象
将 JS 进行生成抽象语法树的网站:astexplorer
比如:var a = 'hello'
语法分析之后生成抽象语法树如下:
{ "type": "Program", "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "a" }, "init": { "type": "Literal", "value": "hello", "raw": "'hello'" } } ], "kind": "var" } ], "sourceType": "script" }
借助网站esprima 或 astexplorer生成。
也可以借助esprima
库生成:
const esprima = require("esprima"); const createAst = (code) => esprima.parseModule(code, { tokens: true }); const code = `var a="hello"`; const ast = createAst(code); console.log(ast);
注意到这里,是代码生成AST然后编译,但也可以用AST转化成代码
看着好像AST跟日常代码有点远,但其实一直在接触:
webpack
和lint
核心都是通过AST
对代码进行检查、分析UglifyJS
通过AST
实现代码压缩混淆babel
核心通过AST而实现代码转换功能
以下要一步步实践~
修改 AST 生成新的代码
AST 怎么转成 代码
借助esprima
将代码转化成AST
,同理可以可借助escodegen
将 AST 转化为代码.
const esprima = require("esprima"); // code变成AST的函数 const createAst = (code) => esprima.parseModule(code, { tokens: true }); const escodegen = require("escodegen"); // AST变成code的函数 let astToCode = (ast) => escodegen.generate(ast); const code = `var a="hello"`; const ast = createAst(code); const newCode = astToCode(ast); // var a = 'hello'; console.log(newCode);
怎么修改 AST
AST 其实就是 JSON 对象,当然怎么修改对象,就怎么修改 AST 啦。
比如想将var a='hello'
变成 const a='hello'
,先看下两个代码的 AST,然后将前者修改成和后者一样就好啦!
细看看只有,kind 那里不一样,这样就简单啦!
// ...同上面 const code = `var a="hello"`; const ast = createAst(code); // 直接修改kind ast.body[0].kind = "const"; const newCode = astToCode(ast); // const a = 'hello'; console.log(newCode);
但简单的这样直接修改很容易,一旦代码变得复杂,嵌套层次变多,或者修改的代码变多,上面的方式就难过了,这里借助另外一个工具库estraverse
,遍历找到需要的地方,然后修改
怎么遍历 AST
AST 虽然是一个 JSON 对象,但是可以以树的结构去理解,而estraverse
是以深度优先的方式遍历 AST 的。
凡是带所有属性 type 的都是一个节点。
也可以用代码直观的理解,estraverse
怎么遍历AST
的:
const esprima = require("esprima"); // code变成AST的函数 const createAst = (code) => esprima.parseModule(code, { tokens: true }); const code = `var a="hello"`; const ast = createAst(code); // 遍历 const estraverse = require("estraverse"); let depth = 0; // 层次越深,缩进就越多 const createIndent = (depth) => " ".repeat(depth); estraverse.traverse(ast, { enter(node) { console.log(`${createIndent(depth)} ${node.type} 进入`); depth++; }, leave(node) { depth--; console.log(`${createIndent(depth)} ${node.type} 离开`); }, });
对照着生成的 AST 看,很明显就是深度遍历的过程:
Program 进入 VariableDeclaration 进入 VariableDeclarator 进入 Identifier 进入 Identifier 离开 Literal 进入 Literal 离开 VariableDeclarator 离开 VariableDeclaration 离开 Program 离开
借助 estraverse 修改 AST
比如:var a='hello'; var b='world'
将var
修改成const
的话
// ..createAst astToCode函数同上面 const estraverse = require("estraverse"); const code = `var a='hello'; var b='world'`; const ast = createAst(code); estraverse.traverse(ast, { enter(node) { // 凡是var的都改成const if (node.kind === "var") { node.kind = "const"; } }, }); const newCode = astToCode(ast); // const a = 'hello'; const b = 'world'; console.log(newCode);
通过estraverse
,很方便的,将旧的 AST增删改其中的结点,从而生成想要的新的 AST!
用 babel-types 快速生成一个 AST
上面的生成一个 AST 总是先有 code 才行,怎么能直接生成 AST 呢?
babel-types
!!!!
AST 是由节点构成的,只要生成相应描述的节点就可以哒。
借助babel-types
可以生成任意的节点!
比如const a='hello',b='world'
:
{ "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "a" }, "init": { "type": "Literal", "value": "hello", "raw": "'hello'" } }, { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "b" }, "init": { "type": "Literal", "value": "world", "raw": "'world'" } } ], "kind": "const" }
其实就是 init + id => VariableDeclarator + kind => VariableDeclaration
const kind = "const"; const id = t.identifier("a"); const init = t.stringLiteral("hello"); const id2 = t.identifier("b"); const init2 = t.stringLiteral("world"); // a=hello const variableDeclarator = t.variableDeclarator(id, init); const variableDeclarator2 = t.variableDeclarator(id2, init2); const declarations = [variableDeclarator, variableDeclarator2]; const ast = t.variableDeclaration(kind, declarations); // 这里的ast就是上面展开的 console.log(ast);
type 名一般就是相应的 API 名,同级的其他属性就是 API 的参数。
怎么写一个 babel 插件
babel
最重要的功能就是将ECMAScript 2015+
版本的代码转换为向后兼容的 JavaScript
语法。
因为转化的地方非常多,babel
一般以插件的形式,通过babel-core
连接插件,从而转换代码。
假设将const
转化为var
的话:
const t = require("babel-types"); // 插件 const ConstPlugin = { visitor: { // path是对应的type的路径,其属性node也可以理解为一个描述const表达式的对象 // 遍历到VariableDeclaration的时候,就把kind换成var VariableDeclaration(path) { let node = path.node; console.log(node); if (node.kind === "const") { node.kind = "var"; } }, }, }; // 试下写的插件 const code = `const a='hello'`; const babel = require("babel-core"); let newCode = babel.transform(code, { plugins: [ConstPlugin] }); // var a = 'hello'; console.log(newCode.code);
手写实现插件 babel-plugin-arrow-functions
babel-plugin-arrow-functions的功能:将箭头函数转化为普通函数
var a = (s) => { return s; }; var b = (s) => s; // 转化成 var a = function (s) { return s; }; var b = function (s) { return s; };
先看下箭头函数和转化成普通函数之后的 AST 区别:
- 注意,body 的类型如果不是
BlockStatement
的话,就换成BlockStatement
;是的话,不用管 - type 是
ArrowFunctionExpression
的节点,可以换成FunctionExpression
节点
因为写babel
插件,所以必须借助babel-core
转换成新代码:
// npm i babel-core babel-types const t = require("babel-types"); // 插件 const ArrowPlugin = { visitor: { ArrowFunctionExpression(path) { let node = path.node; let { params, body } = node; // 如果不是代码块的话,生成一个代码块 if (!t.isBlockStatement(body)) { // 生成return语句 let returnStatement = t.returnStatement(body); // 创建代码块语句 body = t.blockStatement([returnStatement]); } // 利用t生成一个等价的普通函数的表达式对象 const functionJSON = t.functionExpression( null, params, body, false, false ); // 替换掉当前path path.replaceWith(functionJSON); }, }, }; // 试下写的插件 const arrowJsCode = `var a = (s) => { return s; }; var b = (s) => s;`; const babel = require("babel-core"); let functionJSCode = babel.transform(arrowJsCode, { plugins: [ArrowPlugin] }); // var a = function (s) { return s; };var b = function (s) { return s; }; console.log(functionJSCode.code);