程序表面看就是文本文件里的字符,计算机首先对其进行词法、语法分析,然后用某种计算机能理解的低级语言来重新表达程序,而这其中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);
复制代码