一.抽象语法树
抽象语法树(Abstract Syntax Tree)
webpack和Link等很多工具和库的核心都是通过Abstract Syntax Tree抽象语法树这个概念来实现对代码的检查、分析等操作的。
通过了解抽象语法树这个概念,你也可以随意编写类似的工具。
二.抽象语法树用途
- 代码语法的检查、代码风格的检查、代码格式化、代码高亮、代码错误提示、代码自动补全等
- 如JSLint、JSHint对代码错误或风格的检查,发现一些潜在错误
- IDE的错误提示、格式化、高亮、自动补全等
- 代码混淆压缩
- UglifyJS2等
- 优化变更代码,改变代码结构使其达到想要的结构
- 代码打包工具webpack、rollup等
- CommonJS、AMD、CMD、UMD等代码规范之间的转化
- CoffeeScript、TypeScript、JSX等转化为原生Javascript
三.抽象语法树定义
这些工具的原理都是通过Javascript Parser把代码转化为一颗抽象语法树(AST),这颗树定义了代码的结构,通过操纵这棵树,我们可以精准定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作
在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。
Javascript的语法是为了给开发者更好的编程而设计的,但是不适合程序的理解。所以需要转化为AST来使之更适合程序分析,浏览器编译器一般会把源码转化为AST来进行进一步的分析等其他操作。
var AST = "is Tree";
{
"type": "Program",
"body": [{
"type": "VariableDeclaration",
"kind": "var",
"declarations": [{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "AST"
},
"init": {
"type": "Literal", //文本格式
"value": "is Tree",
"raw": "\"is Tree\""
}
}]
}]
}
https://astexplorer.net/
四.JavaScript Parser
- JavaScript Parser,把js源码转化为抽象语法树的解析器
- 浏览器会把js源码通过解析器转化为抽象语法树,再进一步转化为字节码或直接生成机器码
- 一般来说每个js引擎都会有自己的抽象语法树格式,Chrome的v8引擎,FireFox的SpiderMonkey引擎等等,MDN提供了详细的SpiderMonkey AST Format的详细说明,算是业界标准。
4.1 常用JavaScript Parser解析工具有:
- esprima
- traceur
- acorn
- shift
4.2 esprima
- 通过esprima把源码转化为AST
- 通过estraverse遍历并更新AST
- 通过escodegen将AST重新生成源码
- astexplorer AST 可视化工具
npm install esprima estraverse escodegen
let esprima = require('esprima'); //源代码转成AST语法树
let estraverse = require('estraverse'); //遍历语法树
let escodegen = require('escodegen'); //把AST语法树重新生成代码的工具
let sourceCode = 'function ast(){}'
let ast = esprima.parse(sourceCode);
let indent = 0;
function pad(){
return " ".repeat(indent)
}
estraverse.traverse(ast,{
enter(node){
console.log(pad() + node.type);
indent += 2;
},
leave(node){
indent -= 2;
console.log(pad() + node.type)
}
})
五.转化箭头函数
- 访问者模式Visitor对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同
- @babel/core Babel的编译器,核心API都在这里面,比如常见的transform、parse
- babylon Babel的解析器
- babel-types 用于AST节点的Lodash式工具库,它包含了构造,验证以及变换AST节点的方法,对编写处理AST逻辑非常有用
- babel-traverse用于对AST的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点
- babel-types-api
- Babel插件手册
- babeljs.io babel可视化编辑器
- babel-plugin-transform-es2015-arrow-functions babel转化箭头函数
npm install @babel/core babel-types -D
let babel = require('@babel/core'); //用来生成语法树,并且遍历转化语法树
let types = require('babel-types'); //用来生成新节点,或者判断某个节点是否是某个类型
const { generate } = require('escodegen');
const sourceCode = `const sum = (a,b) => a+b`;
//插件的结构
let transformArrayFunction = {
visitor: { //访问者模式;可以访问源代码生成的语法树所有节点,捕获特定节点
//捕获箭头函数表达式,转成普通函数
ArrowFunctionExpression: (path,state) => {
let id = path.parent.id; //path.node代表当前节点,path.parent代表父节点
let arrowNode = path.node;
let params = arrowNode.params;
let body = arrowNode.body; //BinaryExpression
let generator = arrowNode.generator;
let async = arrowNode.async;
//types.blockStatement 生成一个函数体
let functionExpression = types.functionExpression(id,params,types.blockStatement([types.returnStatement(body)]),generator,async)
path.replaceWith(functionExpression); //将转化的新节点替换老节点
}
}
}
let result = babel.transform(sourceCode,{
plugins: [transformArrayFunction],
});
console.log(result);
{
metadata: {},
options: {
babelrc: false,
configFile: false,
passPerPreset: false,
envName: 'development',
cwd: 'd:\\111前端优选\\TODO\\ast',
root: 'd:\\111前端优选\\TODO\\ast',
plugins: [ [Plugin] ],
presets: [],
parserOpts: { sourceType: 'module', sourceFileName: undefined, plugins: [] },
generatorOpts: {
filename: undefined,
auxiliaryCommentBefore: undefined,
auxiliaryCommentAfter: undefined,
retainLines: undefined,
comments: true,
shouldPrintComment: undefined,
compact: 'auto',
minified: undefined,
sourceMaps: false,
sourceRoot: undefined,
sourceFileName: 'unknown'
}
},
ast: null,
code: 'const sum = function sum(a, b) {\n return a + b;\n};', //结果
map: null,
sourceType: 'module'
}
语法树操作三步:
- 根据源代码生成语法树
- 转化语法树
- 根据语法树生成转化后的代码
六.AST
1.解析过程
AST整个解析过程分为两个步骤:
- 分词,将整个代码字符串分割成语法单元数组
- 语法分析,建立分析语法单元之间的关系
2.语法单元
Javascript代码中语法单元主要包括以下几种:
- 关键字:
const
、let、var等 - 标识符:可能是一个变量,也可能是if/else关键字,或者true/false常量
- 运算符
- 数字
- 空格
- 注释
3.词法分析
let sourceCode = `let element = <h1>hello</h1>`;
/**
* 1.分词,把token拆开 词法分析,就是把代码转成一个token数组
*/
function lexical(code){
const tokens = [];
for(let i=0;i<code.length;i++){
let ch = code.charAt(i); //l i=3 ch=空格
if(/[a-zA-Z_]/.test(ch)){ //判断是否为合理变量名、标识符
const token = {type: 'Indentifier',value: ch};
tokens.push(token);
for(i++;i<code.length;i++){ //再向后移,判断是不是英文字母
ch = code.charAt(i); //i=1 ch=e
if(/[a-zA-Z_]/.test(ch)){
token.value += ch; //value=l value=le value=let
}else{ //i=3 ch=空格
if(token.value == 'let'){
token.type = 'KeyWord';
}
i--; //将空格回减
break;
}
}
continue;
}else if(/\s/.test(ch)){ //如果ch是空格的话
const token = {
type: "WhiteSpace",
value: " "
}
tokens.push(token);
for(i++;i<code.length;i++){
ch = code.charAt(i);
if(/\s/.test(ch)){
token.value += ch;
}else{ //关键字和变量名之间的空格(多个)结束
i--;
break;
}
}
continue;
}else if(ch == '='){
const token = {
type: "Equal",
value: "="
};
tokens.push(token);
}else if(ch == '<'){
const token = {
type: 'JSXElement', //遇到小于号,则为JSX元素
value: ch
}
tokens.push(token); //<h1>hello</h1>
let isClose = true; //判断是否遇到闭合标签 <hr/> <h1></h1>
for(i++;i<code.length;i++){
ch = code.charAt(i); //ch = h
token.value += ch;
if(ch == "/"){
isClose = true; //遇到斜杠时则下一个大于号则为闭合标签
}
if(ch == ">"){ //说明标签结束
if(isClose){
break;
}
}
}
continue;
}
}
return tokens;
}
let tokens = lexical(sourceCode);
console.log(tokens);
/**
[
{ type: 'KeyWord', value: 'let' },
{ type: 'WhiteSpace', value: ' ' },
{ type: 'Indentifier', value: 'element' },
{ type: 'WhiteSpace', value: ' ' },
{ type: 'Equal', value: '=' },
{ type: 'WhiteSpace', value: ' ' },
{ type: 'JSXElement', value: '<h1>' },
{ type: 'Indentifier', value: 'hello' },
{ type: 'JSXElement', value: '</h1>' }
]
*/
4.语法分析
- 语义分析则是将得到的词汇进行一个立体的组合,确定词语之间的关系
- 简单来说语法分析是对语句和表达式的识别,是一个递归的过程
function parse(tokens){
let ast = {
type: 'Program',
body: [],
sourceType: 'module'
};
let i = 0; //当前的索引
let currentToken; //当前的token
while(currentToken = tokens[i]){
//第一次的时候 currentToken = { type: 'KeyWord', value: 'let' }
if(currentToken.type == 'KeyWord' && currentToken.value == 'let'){ //或者var/const
let VariableDeclaration = {
type: 'VariableDeclaration',declarations: []
};
ast.body.push(VariableDeclaration);
i+=2; //{ type: 'Indentifier', value: 'element' },
currentToken = tokens[i];
let variableDeclarator = {
type: 'VariableDeclarator',
id: {
type: 'Indentifier',name: currentToken.value
},
}
VariableDeclaration.declarations.push(variableDeclarator);
i+=2; //i=4 //
currentToken = tokens[i]; //{ type: 'JSXElement', value: '<h1>hello</h1>' },
if(currentToken.type == "String"){
variableDeclarator.init = {type: 'StringLiteral',value: currentToken.value}
}else if(currentToken.type == "JSXElement"){
let value = currentToken.value;
let [,type,children] = value.match(/<([^>]+?)>([^>]+)<\/\1>/); //<h1></h1> type=h1 children=hello
variableDeclarator.init = {
type: 'JSXElement', //JSX元素
openingElement: {
type: 'openingElement',
name: {
type: 'JSXIndetifier',
name: type
}
},
closingElement: {
type: 'closingElement',
name: {
type: 'JSXIndentifier',
name: type
}
},
children: [
{type: 'JSXElement',value: children}
]
}
}
}
i++;
}
return ast;
}
let tokens = [
{ type: 'KeyWord', value: 'let' },
{ type: 'WhiteSpace', value: ' ' },
{ type: 'Indentifier', value: 'element' },
{ type: 'WhiteSpace', value: ' ' },
{ type: 'Equal', value: '=' },
{ type: 'WhiteSpace', value: ' ' },
{ type: 'JSXElement', value: '<h1>hello</h1>' }
]
let ast = parse(tokens);
ast.body[0].declarations[0].init = {
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"computed": false,
"object": {
"type": "Indentifier",
"name": "React"
},
"property": {
"type": "Indentifier",
"name": "createElement"
}
},
"arguments": [
{
"type": "Literal",
"value": "h1",
"raw": "\"h1\""
},
{
"type": "Literal",
"value": null,
"raw": "null"
},
{
"type": "Literal",
"value": "hello",
"raw": "\"hello\""
}
]
}
}
console.log(JSON.stringify(ast));
/**
* {"type":"Program","body":[{"type":"VariableDeclaration","declarations":[{"type":"VariableDeclarator","id":{"type":"Indentifier","name":"element"},"init":{"type":"ExpressionStatement","expression":{"type":"CallExpression","callee":{"type":"MemberExpression","computed":false,"object":{"type":"Indentifier","name":"React"},"property":{"type":"Indentifier","name":"createElement"}},"arguments":[{"type":"Literal","value":"h1","raw":"\"h1\""},{"type":"Literal","value":null,"raw":"null"},{"type":"Literal","value":"hello","raw":"\"hello\""}]}}}]}],"sourceType":"module"}
*/
至此就简单实现了语法树的转化,主要是在于思路上对源码到语法树解析的三个步骤。通过语法树原理的分析,有助于我们对Babel,Webpack等编译插件的原理分析,并应用于日常开发中。
获取更多前端资讯,欢迎搜索并关注公众号【前端优选】
本文使用 mdnice 排版