当前位置: 首页 > 科技观察

JavaScript语法树和代码转换实践

时间:2023-03-21 13:24:34 科技观察

JavaScript语法树和代码转换实践在作者的现代JavaScript开发:语法基础和实战技巧系列文章中有总结。本文引用的参考资料声明在JavaScript学习与实践资料索引中。特别声明,部分代码片段引用自BabelHandbook开源手册;也欢迎大家关注前端周榜系列,获取第一手资讯。JavaScript语法树和代码转换浏览器的兼容性一直是前端项目开发的难点之一。往往客户端浏览器的升级无法与语法特性的迭代保持一致;因此,我们需要使用大量的polyfill。,以确保在生产环境中用现代语法编写的JavaScript在浏览器中流畅运行,从而在可用性和代码可维护性之间取得更好的平衡。以Babel为代表的语法转换工具可以帮助我们自动将ES6等现代JavaScript代码转换为ES5或其他可在旧浏览器中运行的等效实现;其实Babel不仅仅是一个语法解析器,还是一个插件丰富的平台,稍加扩展就可以应用于前端监控埋点、错误日志收集等场景。作者还使用Babel和Babylon实现了swagger-decorator的flowToDecorator功能,可以自动从Flow文件中提取类型信息,并为类属性添加适当的注解。Babel从Babel6开始,核心的babel-core只暴露了部分核心接口,使用Babylon构建语法树,即上图中的Parse和Generate步骤;实际的转换步骤是由配置的插件(Plugin)完成的。所谓Preset就是一系列插件的集合。比如在babel-preset-es2015的源码中定义了一系列的插件:,transformES2015BlockScopedFunctions,[transformES2015Classes,optsLoose],transformES2015ObjectSuper,...modules==="commonjs"&&[transformES2015ModulesCommonJS,optsLoose],modules==="systemjs"&&[transformES2015ModulesSystemJS,optsLoose],modules==="amd"&&[transformES2015ModulesAMD,optsLoose],modules==="umd"&&[transformES2015ModulesUMD,optsLoose],[transformRegenerator,{async:false,asyncGenerators:false}]].filter(Boolean)//filteroutfalsyvalues};Babel可以根据不同的配置对输入的JavaScript代码进行适当的转换。主要步骤是Parse、Transform和Generate:在解析步骤中,Babel使用LexicalAnalysis和SyntacticAnalysis将输入的代码转换成抽象语法树;词法分析步骤将代码转换为令牌流,语法分析步骤将令牌流转换为语言内置AST表示。在转换步骤中,Babel会遍历上一步生成的token流,根据配置进行添加、更新、移除节点等操作;Babel本身不进行转换操作,而是依赖外部插件进行实际转换。最新的代码生成是将上一步转换的抽象语法树重新生成为代码,同时创建SourceMap;代码生成比前面两步简单很多,其核心思想是先深入遍历抽象语法树,然后生成对应的代码串。AbstractSyntaxTree抽象语法树(AST)的作用是牢牢抓住程序的上下文,以便于在编译过程的后续步骤(如代码生成)中对程序进行解释。AST是开发人员为该语言量身定制的一组模型。基本上,语言中的每个结构都对应一个AST对象。上面提到的解析步骤中的词法分析步骤会将代码转换成所谓的token流,例如对于代码n*n,它会转换成如下数组:[{type:{...},值:"n",start:0,end:1,loc:{...}},{type:{...},value:"*",start:2,end:3,loc:{...}},{type:{...},value:"n",start:4,end:5,loc:{...}},...]其中每个类型都是一系列描述token一组属性:{type:{label:'name',keyword:undefined,beforeExpr:false,startsExpr:true,rightAssociative:false,isLoop:false,isAssign:false,prefix:false,postfix:false,binop:null,updateContext:null},...}这里的每一个类型都类似于AST中的节点,有start、end、loc等属性;在实际应用中,例如对于ES6中的箭头函数,我们可以通过babylon解释器生成如下的AST表示:{type:{label:'name',keyword:undefined,beforeExpr:false,startsExpr:true,rightAssociative:false,isLoop:false,isAssign:false,prefix:false,postfix:false,binop:null,updateContext:null},...}我们可以使用工具ASTExplorer进行在线预览和编辑;在上面的AST表示中,顾名思义,ArrowFunctionExpression表示该表达式是一个箭头函数表达式。这个函数有两个参数,foo和bar。参数的Identifiers类型是没有任何子节点的变量名类型;然后我们发现加号运算符表示为BinaryExpression类型,其运算符属性设置为+,而left和right两个参数分别挂载在left和right属性下。在接下来的转换步骤中,我们需要转换这样一个抽象语法树。这一步主要由BabelPreset和Plugin控制;Babel内部提供了库babel-traverse来辅助AST遍历。该库还提供了一系列内置的替换和操作接口。转换后的AST表示如下。在实际开发中,我们往往先对比转换前后代码的AST表示,了解应该进行什么样的转换操作://ASTshortenedforclarity{"program":{"type":"Program","body":[{"type":"ExpressionStatement","expression":{"type":"Literal","value":"usestrict"}},{"type":"ExpressionStatement","expression":{"type":"FunctionExpression","async":false,"params":[{"type":"Identifier","name":"foo"},{"type":"Identifier","name":"bar"}]"body":{"type":"BlockStatement","body":[{"type":"ReturnStatement","argument":{"type":"BinaryExpression","left":{"type":"标识符","名称":"foo"},"运算符":"+","right":{"type":"Identifier","name":"bar"}}}]},"parenthesizedExpression":true}}]}}自定义插件Babel支持在Observer模式下定义插件,我们可以在visitor中预先设置好我们想要观察的Babel节点类型,然后进行操作;比如我们需要将下面的箭头函数源码转换成ES5中的函数定义://SourceCodeconstfunc=(foo,bar)=>foo+bar;//TransformedCode"usestrict";const_func=function(_foo,_bar){return_foo+_bar;};上一节我们比较了两个函数语法树变换前后的区别,这里开始定义Transformation插件首先,每个插件都以一个babel对象作为输入参数,返回一个包含访问者对象的函数。***我们需要调用babel-core提供的transform函数来注册插件,并指定要转换的源码或源码文件://plugin.js文件,定义插件importtypeNodePathfrom"babel-traverse";exportdefaultfunction(babel){const{types:t}=babel;return{name:"ast-transform",//notrequiredvisitor:{Identifier(path){path.node.name=`_${path.node.name}`;},ArrowFunctionExpression(path:NodePath,state:Object){//Insomeconversioncases,itmayhavealreadybeenconvertedtoafunctionwhilethiscallback//wasqueuedup.if(!path.isArrowFunctionExpression())return;path.arrowFunctionToExpression({//Whileotherutilsmmaybefineinsertingotherarrowstomakemoretransformspossible,//箭头转换本身绝对不能插入新箭头函数.state!!optArrowfalse});}}};}//babel.js使用插件varbabel=require('babel-core');varplugin=require('./plugin');varout=babel.转换(src,{插件:[插件]});常用的变换操作遍历获取子节点路径我们可以通过path.node.{property}访问AST中的节点属性://theBinaryExpressionASTnodehasproperties:`left`,`right`,`operator`BinaryExpression(path){path.node.left;path.node.right;path.node.operator;}我们也可以使用路径对象的get方法,通过传入子路径的字符串表示来访问一个属性:BinaryExpression(path){path.get('left');}Program(path){path.get('body.0');}判断一个节点是否指定类型的内置类型对象提供了很多工具函数,可以直接用来判断节点类型:BinaryExpression(path){if(t.isIdentifier(path.node.left)){//...}}或者同时用浅比较查看节点属性:BinaryExpression(path){if(t.isIdentifier(path.node.left,{name:"n"})){//...}}//等价到BinaryExpression(path){if(path.node.left!=null&&path.node.left.type==="Identifier"&&path.node.left.name==="n"){//...}}判断对应的路径是指定类型的节点BinaryExpression(path){if(path.get('left').isIdentifier({name:"n"})){//...}}获取父节点的指定路径有时候我们需要从指定节点向上遍历获取父节点。这时候我们可以通过传入检测回调来判断:path.findParent((path)=>path.isObjectExpression());//获取最近的函数Declarenodepath.getFunctionParent();获取兄弟路径如果路径存在于函数或程序中的类列表结构中,则它可能包含兄弟路径://sourcecodevara=1;//pathA,path.key=0varb=2;//pathB,path.key=1varc=3;//pathC,path.key=2//插件定义exportdefaultfunction({types:t}){return{visitor:{VariableDeclaration(path){//ifthecurrentpathispathApath.inList//truepath.listKey//"body"path.key//0path.getSibling(0)//pathApath.getSibling(path.key+1)//pathBpath.container//[pathA,pathB,pathC]}}};}停止遍历在某些情况下,插件需要停止遍历,此时我们只需要在插件中添加一个返回表达式即可:BinaryExpression(path){if(path.node.operator!=='**')return;}我们也可以指定忽略遍历某个子路径:outerPath.traverse({Function(innerPath){innerPath.skip();//如果检查children是无关紧要的},ReferencedIdentifier(innerPath,state){state.iife=true;innerPath.stop();//ifyouwanttosavesomestateandthenstoptraversal,ordeopt}});操作替换节点//插件定义BinaryExpression(path){path.replaceWith(t.binaryExpression("**",path.node.left,t.numberLiteral(2)));}//代码结果functionsquare(n){-returnn*n;+returnn**2;}用多个节点替换一个节点//插件定义ReturnStatement(path){path.replaceWithMultiple([t.expressionStatement(t.stringLiteral("Isthisthereallife?")),t.expressionStatement(t.stringLiteral("Isthisjustfantasy?")),t.expressionStatement(t.stringLiteral("(Enjoysingingtherestofthesonginyourhead)")),]);}//代码结果函数square(n){-returnn*n;+"Isthisthereallife?";+"Isthisjustfantasy?";+"(Enjoysingtherestofthesonginyourhead)";}用源代码字符串替换节点//插件定义FunctionDeclaration(path){path.replaceWithSourceString(`functionadd(a,b){returna+b;}`);}//代码结果-functionsquare(n){-returnn*n;+functionadd(a,b){+returna+b;}insertsiblingnode//插件定义FunctionDeclaration(path){path.insertBefore(t.expressionStatement(t.stringLiteral("BecauseI'mmeasycome,easygo.")));path.insertAfter(t.expressionStatement(t.stringLiteral("Alittlehigh,littlelow.")));}//coderesult+"因为我很容易来,很容易去。";functionsquare(n){return*n;}+"Alittlehigh,littlelow.";删除节点//插件定义FunctionDeclaration(path){path.remove();}//代码结果-functionsquare(n){-returnn*n;-}替换节点//插件定义BinaryExpression(path){path.parentPath.replaceWith(t.expressionStatement(t.stringLiteral("Anywaythewindblows,doesn'ttreallymattertome,tome.")));}//CodeResultfunctionsquare(n){-return*n;+"Anywaythewindblows,doesn'treallymattertome,tome.";}移除一个父节点//插件定义BinaryExpression(path){path.parentPath.remove();}//代码结果functionsquare(n){-returnn*n;}scope判断一个局部变量是否绑定:FunctionDeclaration(path){if(path.scope.hasBinding("n")){//...}}FunctionDeclaration(path){if(path.scope.hasOwnBinding("n")){//...}}创建UIDFunctionDeclaration(path){path.scope.generateUidIdentifier("uid");//Node{type:"Identifier",name:"_uid"}path.scope.generateUidIdentifier("uid");//Node{type:"Identifier",name:"_uid2"}}提取一个变量声明到sideeffect//plugindefineFunctionDeclaration(路径){constid=path.scope.generateUidIdentifierBasedOnNode(path.node.id);path.remove();path.scope.parent.push({我d,init:path.node});}//代码结果-functionsquare(n){+var_square=functionsquare(n){returnn*n;-}+};