抽象语法树AST必知必会 | 京东物流技术团队
1 介绍 AST
打开前端项目中的 package.json,会发现众多工具已经占据了我们开发日常的各个角落,例如 JavaScript 转译、CSS 预处理、代码压缩、ESLint、Prettier 等。这些工具模块大都不会交付到生产环境中,但它们的存在于我们的开发而言是不可或缺的。
有没有想过这些工具的功能是如何实现的呢?没错,抽象语法树 (Abstract Syntax Tree) 就是上述工具的基石。
Babel,Webpack,Vue-cli 和 EsLint 等很多的工具和库的核心都是通过 Abstract Syntax Tree 抽象语法树这个概念来实现对代码的检查、分析等操作的。在前端当中 AST 的使用场景非常广,比如在 Vue.js 当中,我们在代码中编写的 template 转化成 render function 的过程当中第一步就是解析模版字符串生成 AST。
AST 的官方定义:
抽象语法树 (Abstract Syntax Tree,AST),是源代码语法结构的一种抽象表示。以树状的形式表现编程语言的语法结构,每个节点都表示源代码中的一种结构。
JS 的许多语法为了给开发者更好的编程体验,并不适合程序的理解。所以需要把源码转化为 AST 来更适合程序分析,浏览器的编译器一般会把源码转化为 AST 来进行进一步的分析来进行其他操作。通过了解 AST 这个概念,对深入了解前端的一些框架和工具是很有帮助的。
那么 AST 是如何生成的?为什么需要 AST ?
了解过编译原理的同学知道计算机想要理解一串源代码需要经过“漫长”的分析过程:
- 词法分析 (Lexical Analysis)
- 语法分析 (Syntax Analysis)
- …
- 代码生成 (Code Generation)
这是在线的 AST 转换器:AST转换器。可以在这个网站上,亲自尝试下转换。点击语句中的词,右边的抽象语法树节点便会被选中,如下图:
代码转化成 AST 后的格式大致如下图所示:
为了方便大家理解抽象语法树,来看看具体的例子。
var tree = 'this is tree'
js 源代码将会被转化成下面的抽象语法树:
{ "type": "Program", "start": 0, "end": 25, "body": [ { "type": "VariableDeclaration", "start": 0, "end": 25, "declarations": [ { "type": "VariableDeclarator", "start": 4, "end": 25, "id": { "type": "Identifier", "start": 4, "end": 8, "name": "tree" }, "init": { "type": "Literal", "start": 11, "end": 25, "value": "this is tree", "raw": "'this is tree'" } } ], "kind": "var" } ], "sourceType": "module" }
可以看到一条语句由若干个词法单元组成。这个词法单元就像 26 个字母。创造出个十几万的单词,通过不同单词的组合又能写出不同内容的文章。
字符串形式的 type 字段表示节点的类型。比如”BlockStatement”,”Identifier”,”BinaryExpression”等。 每一种类型的节点定义了一些属性来描述该节点类型,然后就可以通过这些节点来进行分析其他操作。
2 AST 如何生成
看到这里,你应该已经知道抽象语法树大致长什么样了。那么AST又是如何生成的呢?
以上面var tree = ‘this is tree’为例:
词法分析
其中词法分析阶段扫描输入的源代码字符串,生成一系列的词法单元 (tokens),这些词法单元包括数字,标点符号,运算符等。词法单元之间都是独立的,也即在该阶段我们并不关心每一行代码是通过什么方式组合在一起的。
大致可以看出转换之前源代码的基本构造。
语法分析阶段——老师教会我们每个单词在整个句子上下文中的具体角色和含义。
- 代码生成
最后是代码生成阶段,该阶段是一个非常自由的环节,可由多个步骤共同组成。在这个阶段我们可以遍历初始的 AST,对其结构进行改造,再将改造后的结构生成对应的代码字符串。
代码生成阶段——我们已经弄清楚每一条句子的语法结构并知道如何写出语法正确的英文句子,通过这个基本结构我们可以把英文句子完美地转换成一个中文句子。
3 AST 的基本结构
抛开具体的编译器和编程语言,在 “AST 的世界”里所有的一切都是节点 (Node),不同类型的节点之间相互嵌套形成一颗完整的树形结构。
{ "program": { "type": "Program", "sourceType": "module", "body": [ { "type": "FunctionDeclaration", "id": { "type": "Identifier", "name": "foo" }, "params": [ { "type": "Identifier", "name": "x" } ], "body": { "type": "BlockStatement", "body": [ { "type": "IfStatement", "test": { "type": "BinaryExpression", "left": { "type": "Identifier", "name": "x" }, "operator": ">", "right": { "type": "NumericLiteral", "value": 10 } } } ] } ... } ... ] }
AST 的结构在不同的语言编译器、不同的编译工具甚至语言的不同版本下是各异的,这里简单介绍一下目前 JavaScript 编译器遵循的通用规范 —— ESTree 中对于 AST 结构的一些基本定义,不同的编译工具都是基于此结构进行了相应的拓展。
4 AST 的应用场景和用法
了解 AST 的概念和具体结构后,你可能不禁会问:AST 有哪些使用场景,怎么用?
代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
- 如 JSLint、JSHint 对代码错误或风格的检查,发现一些潜在的错误。
- IDE 的错误提示、格式化、高亮、自动补全等等。
代码混淆压缩。
- UglifyJS2 等。
优化变更代码,改变代码结构使达到想要的结构。
- 代码打包工具 webpack、rollup 等等。
- CommonJS、AMD、CMD、UMD 等代码规范之间的转化。
- CoffeeScript、TypeScript、JSX 等转化为原生 Javascript。
至于如何使用 AST ,归纳起来可以把它的使用操作分为以下几个步骤:
- 解析 (Parsing):这个过程由编译器实现,会经过词法分析过程和语法分析过程,从而生成 AST。
- 读取/遍历 (Traverse):深度优先遍历 AST ,访问树上各个节点的信息(Node)。
- 修改/转换 (Transform):在遍历的过程中可对节点信息进行修改,生成新的 AST。
- 输出 (Printing):对初始 AST 进行转换后,根据不同的场景,既可以直接输出新的 AST,也可以转译成新的代码块。
通常情况下使用 AST,我们重点关注步骤2和3,诸如 Babel、ESLint 等工具暴露出来的通用能力都是对初始 AST 进行访问和修改。
这两步的实现基于一种名为访问者模式的设计模式,即定义一个 visitor 对象,在该对象上定义了对各种类型节点的访问方法,这样就可以针对不同的节点做出不同的处理。例如,编写 Babel 插件其实就是在构造一个 visitor 实例来处理各个节点信息,从而生成想要的结果。
const visitor = { CallExpression(path) { ... } FunctionDeclaration(path) { ... } ImportDeclaration(path) { ... } ... } traverse(AST, visitor)
5 AST 的转化流程
利用 babel-core (babel 核心库,实现核心的转换引擎) 和 babel-types (可以实现类型判断,生成 AST 节点等)和 AST 来将
let sum = (a, b) => a + b
改成为:
let sum = function(a, b) { return a + b }
实现代码如下:
// babel核心库,实现核心的转换引擎 let babel = require('babel-core'); // 可以实现类型判断,生成AST节点等 let types = require('babel-types'); let code = `let sum = (a, b) => a + b`; // let sum = function(a, b) { // return a + b // } // 这个访问者可以对特定类型的节点进行处理 let visitor = { ArrowFunctionExpression(path) { console.log(path.type); let node = path.node; let expression = node.body; let params = node.params; let returnStatement = types.returnStatement(expression); let block = types.blockStatement([ returnStatement ]); let func = types.functionExpression(null,params, block,false, false); path.replaceWith(func); } } let arrayPlugin = { visitor } // babel内部会把代码先转成AST, 然后进行遍历 let result = babel.transform(code, { plugins: [ arrayPlugin ] }) console.log(result.code);
分词将整个代码字符串分割成最小语法单元数组,生成 AST 抽象语法树,经过转化 transformer 生成新的 AST 树,遍历生成最终想要的结果 genrator:
AST 的三板斧:
- 通过 esprima 生成 AST
- 通过 estraverse 遍历和更新 AST
- 通过 escodegen 将 AST 重新生成源码
我们可以来做一个简单的例子:
1.先新建一个 test 的工程目录。
2.在 test 工程下安装 esprima、estraverse、escodegen 的 npm 模块
npm i esprima estraverse escodegen --save
3.在目录下面新建一个 test.js 文件,载入以下代码:
const esprima = require('esprima'); let code = 'const a = 1'; const ast = esprima.parseScript(code); console.log(ast);
将会看到输出结果:
Script { type: 'Program', body: [ VariableDeclaration { type: 'VariableDeclaration', declarations: [Array], kind: 'const' } ], sourceType: 'script' }
4.再在 test 文件中,载入以下代码:
const estraverse = require('estraverse'); estraverse.traverse(ast, { enter: function (node) { node.kind = "var"; } }); console.log(ast);
5.最后在 test 文件中,加入以下代码:
const escodegen = require("escodegen"); const transformCode = escodegen.generate(ast) console.log(transformCode);
输出的结果:
var a = 1;
通过这三板斧:我们将const a = 1转化成了var a = 1
6 实际应用
利用 AST 实现预计算的 Babel 插件,实现代码如下:
// 预计算简单表达式的插件 let code = `const result = 1000 * 60 * 60`; let babel = require('babel-core'); let types= require('babel-types'); let visitor = { BinaryExpression(path) { let node = path.node; if (!isNaN(node.left.value) && ! isNaN(node.right.value)) { let result = eval(node.left.value + node.operator + node.right.value); result = types.numericLiteral(result); path.replaceWith(result); let parentPath = path.parentPath; // 如果此表达式的parent也是一个表达式的话,需要递归计算 if (path.parentPath.node.type == 'BinaryExpression') { visitor.BinaryExpression.call(null, path.parentPath) } } } } let cal = babel.transform(code, { plugins: [ {visitor} ] });
作者:京东物流 李琼
来源:京东云开发者社区 自猿其说Tech

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
深入理解 Serverless 计算的并发度
背景 2019 年 Berkeley 预测 Serverless 将取代 Serverful 计算[1],成为云计算的计算新范式。Serverless 为应用程序开发提供了一种全新的系统架构,其凭借着弹性伸缩省事省心,按需付费更低成本、聚焦业务降低 OPS 这三大核心价值,将开发人员从繁重的手动资源管理和性能成本优化中解放出来,让工程师的生产力再次发生变革。 根据 CNCF 官方定义[2]: “Serverless is a cloud native development model that allows developers to build and run applications without having to manage servers. There are still servers in serverless, but they are abstracted away from app development. A cloud provider handles the routine work of provisioning, maintaining, and s...
- 下一篇
Ui2Code+ChatGPT助力低代码搭建 | 京东云技术团队
前言 低代码开发平台(LCDP),是低代码或无代码通过快速搭建配置的方式完成一个应用程序的开发与上线,可视化低代码就是可视化的DSL,它的优点更多的是来源可视化,相对的,它的局限性也还是来源于可视化,复杂的业务逻辑用低代码可能会更加复杂。低代码应该是特定领域问题的简化和抽象,如果只是单纯将原有的编码工作转换为 GUI 的模式,并没有多大意义。 背景 随着京东微信域业务与腾讯合作的加深,作为流量的载体,小程序的需求日益增多,自17年开始 c-1、c-2、c-3 等部门都有各自的业务小程序,至今为止集团内上万个微信小程序,如此多的小程序是否存在共性,是否可以互相赋能,答案是肯定的,基于种种考虑,我们开始了小程序搭建方面的调研与规划 架构设计 1、思考 搭建的本质是提效,那么到底是低学习成本快速上手,垂直于某条业务线好,还是足够通用可以满足任何业务场景,但学习成本高更好 太通用:接入成本高、学习成本高、开发成本高 太垂直:接入效率高、学习成本低、扩展能力差 2、功能 1、零代码或低代码快速生成应用 2、提供可视化界面进行开发 3、通过拖拽+配置实现项目搭建 3、分层架构+iOC架构 分层,系...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS关闭SELinux安全模块
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Hadoop3单机部署,实现最简伪集群
- CentOS6,7,8上安装Nginx,支持https2.0的开启