当前位置: 首页 > 后端技术 > Node.js

AST抽象语法树

时间:2023-04-03 10:30:53 Node.js

阅读AST原文AST抽象语法树简介AST(AbstractSyntaxTree)是源代码抽象语法结构的树状表示。Webpack、ESLint、JSX、TypeScript的编译和模块化规则之间的转换是通过AST来实现代码的检查、分析和编译。JavaScript语法之AST语法树如果想在JavaScript中使用AST进行开发,需要知道抽象成语法树后的结构是什么,其中的字段名是什么意思,遍历规则,可以通过http://esprima.org/demo/parse...实现JavaScript语法的在线转换。借助在线编译工具,可以将functionfn(a,b){}编译成如下结构。{“类型”:“程序”,“正文”:[{“类型”:“FunctionDeclaration”,“id”:{“类型”:“标识符”,“名称”:“fn”},“参数”:[{"type":"Identifier","name":"a"},{"type":"Identifier","name":"b"}],"body":{"type":"BlockStatement","body":[]},"generator":false,"expression":false,"async":false}],"sourceType":"script"}将JavaScript语法编译成抽象语法树后,需要遍历、修改、重新编译,遍历树结构的过程是“顺序深度优先”。esprima、estraverse和escodegeneesprima、estraverse和escodegen模块是操作AST的三个重要模块,也是实现babel的核心依赖。下面分别介绍这三个模块的功能。1、esprima将JS转成ASTesprima模块的用法如下://file:esprima-test.jsconstesprima=require("esprima");letcode="functionfn(){}";//生成语法treelettree=esprima.parseScript(code);console.log(tree);//Script{//type:'Program',//body://[FunctionDeclaration{//type:'FunctionDeclaration',//id:[Identifier],//params:[],//body:[BlockStatement],//generator:false,//expression:false,//async:false}],//sourceType:'script'}通过上面的案例,可以看出是通过esprima模块的parseScript方法将JS代码块转换成语法树,需要将代码块转换成字符串,或者通过parseModule转换成一个模块方法。2.Estraverse遍历修改AST查看遍历过程://文件:estraverse-test.jsconstesprima=require("esprima");constestraverse=require("estraverse");letcode="functionfn(){}";//遍历语法树etraverse.traverse(esprima.parseScript(code),{enter(node){console.log("enter",node.type);},leave(){console.log("leave",node.type);}});//enterProgram//enterFunctionDeclaration//enterIdentifier//leaveIdentifier//enterBlockStatement//leaveBlockStatement//leaveFunctionDeclaration//leaveProgram上面的代码将是通过etraverse模块的traverse方法遍历esprima模块转换的AST,打印打印所有类型属性。每个包含类型属性的对象称为节点。修改是获取对应的类型,修改节点中的属性。其实AST的深度遍历就是遍历每一层的type属性,所以遍历会分为入口阶段和出口阶段两个阶段。在estraverse的traverse方法中,分别使用参数指定的entry和leave函数进行监听,但是我们一般只使用entry。3.escodegen将AST转JS下面这个案例就是将一段JS代码块转成AST,并将遍历修改后的AST重新转成JS的全过程。//文件:escodegen-test.jsconstesprima=require("esprima");constestraverse=require("estraverse");constescodegen=require("escodegen");letcode="functionfn(){}";//生成语法树lettree=esprima.parseScript(code);//遍历语法树etraverse.traverse(tree,{enter(node){//修改函数名if(node.type==="FunctionDeclaration"){node.id.name="ast";}}});//编译语法树letresult=escodegen.generate(tree);console.log(result);//函数ast(){//}在遍历AST过程中params的值是一个数组,没有type属性。实现Babel语法转换插件实现语法转换插件需要用到babel-core和babel-types两个模块。其实这两个模块都依赖esprima、estraverse和escodegen。这两个模块的使用需要安装,命令如下:npminstallbabel-corebabel-types1,plugin-transform-arrow-functionsplugin-transform-arrow-functions是babel家族的一员,用来将箭头函数转换为ES5语法函数表达式。//文件:plugin-transform-arrow-functions.jsconstbabel=require("babel-core");consttypes=require("babel-types");//箭头函数代码块letsumCode=`constsum=(a,b)=>{returna+b;}`;letminusCode=`constminus=(a,b)=>a-b;`;//转换ES5插件letArrowPlugin={//visitor(访问或模式)visitor:{//path是树的路径ArrowFunctionExpression(path){//获取树节点让节点=路径。节点;//获取参数和函数体letparams=node.params;让body=node.body;//判断函数体是否为代码块,如果不是,则添加return和{}if(!types.isBlockStatement(body)){letreturnStatement=types.returnStatement(body);body=types.blockStatement([returnStatement]);}//生成函数表达式树结构letfunc=types.functionExpression(null,params,body,false,false);//用新的树结构替换旧的树结构types.replaceWith(func);}}};//生成转换后的代码块letsumResult=babel.transform(sumCode,{plugins:[ArrowPlugin]});letminusResult=babel.transform(minusCode,{plugins:[ArrowPlugin]});console.log(sumResult.code);console.log(minusResult.code);//letsum=function(a,b){//returna+b;//};//letminus=function(a,b){//returna-b;//};我们主要是使用babel-core的transform方法将AST转化为代码块。第一个参数是转换前的代码块(字符串),第二个参数是一个配置项,其中plugins的值是一个数组,存放的是babal-core转换后修改后的AST插件(对象),之后使用transform方法将旧的AST处理成新的代码块,返回值是一个对象,对象的code属性是babel-types模块提供的转换后的代码块(字符串)内部修改方法实现,API可以在https://github.com/babel/babe查看...ArrowPlugin是传入transform方法的插件。必须包含visitor属性(固定),value也是一个对象。用于存放修改语法树的方法。方法名必须严格遵循API,对应的方法会修改AST的对应节点。types.functionExpression方法中,参数表示,函数名(匿名函数为null),函数参数(必填),函数体(必填),是否为生成器函数(默认false),是否为异步函数(defaultfalse),返回值为修改后的AST,types.replaceWith方法用于替换AST,参数为新的AST。2.plugin-transform-classesplugin-transform-classes也是Babel家族的一员,用于将ES6类类转换为ES5构造函数。//文件:plugin-transform-classes.jsconstbabel=require("babel-core");consttypes=require("babel-types");//classletcode=`classPerson{constructor(name){this.name=name;}getName(){返回this.name;}}`;//将类转换为ES5构造函数插件letClassPlugin={visitor:{ClassDeclaration(path){letnode=path.node;让classList=node.body.body;//将获取的类名转换为标识符{type:'Identifier',name:'Person'}letclassName=types.identifier(node.id.name);让身体=类型。块语句([]);让func=types.functionDeclaration(className,[],body,false,false);path.replaceWith(函数);//用于存放多个原型方法letes5Func=[];//获取类中的代码体classList.forEach((item,index)=>{//函数的代码体letbody=classList[index].body;//获取参数letparams=item.params。长度?item.params.map(val=>val.name):[];//将参数转换为标识符params=types.identifier(params);//判断是否是构造函数,如果是构造函数,则生成一个新的函数替换params],body,false,false);}else{//其他情况是原型方法letproto=types.memberExpression(className,types.identifier("prototype"));//左层定义标识符Person.prototype.getNameletleft=types.memberExpression(proto,types.identifier(item.key.name));//定义右边的匿名函数letright=types.functionExpression(null,[params],body,false,false);//合并左右两边,存入数组es5Func。推(types.assignmentExpression(“=”,左,右));}});//如果没有原型方法,直接替换if(es5Func.length===0){path.replaceWith(func);}else{es5Func.push(func);//替换n个节点path.replaceWithMultiple(es5Func);}}}};//生成转换后的代码块result=babel.transform(code,{plugins:[ClassPlugin]});控制台日志(结果代码);//Person.prototype.getName=function(){//returnthis.name;//}//functionPerson(name){//this.name=name;//}上面插件的实现比较复杂比插件转换箭头功能。归根结底,是ES6和ES5的语法树会互相转换,找到它们的不同点,使用babel-types提供的API修改语法树对应的节点属性,替换语法树。值得注意的是,path.replaceWithMultiple与path.replaceWith不同。参数是一个数组,array支持多种语法树结构,可以根据修改语法树的具体场景来选择,也可以根据不同的情况使用不同的替换方式。小结通过这一节,我们了解了什么是AST抽象语法树以及抽象语法树在JavaScript中的具体体现以及NodeJS中AST抽象语法树生成、遍历、修改的核心依赖,以及使用babel-core和babel-core这两个模块babel-types来轻松模拟ES6新特性转成ES5语法的过程,希望能为以后自己实现一些编译插件提供思路。