您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 
 订阅
手把手带你入门 AST 抽象语法树
 
 
  1336  次浏览      24 次
 2023-10-13
 
编辑推荐:
本文主要介绍了AST 抽象语法树入门知识。 希望能为大家提供一些参考或帮助。
文章来自于稀土掘金,由火龙果Linda编辑推荐。

AST 是什么

抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

AST 有什么用

AST 运用广泛,比如:

编辑器的错误提示、代码格式化、代码高亮、代码自动补全;

elint、pretiier 对代码错误或风格的检查;

webpack 通过 babel 转译 javascript 语法;

并且如果你想了解 js 编译执行的原理,那么你就得了解 AST。

AST 如何生成

js 执行的第一步是读取 js 文件中的字符流,然后通过词法分析生成 token,之后再通过语法分析( Parser )生成 AST,最后生成机器码执行。

整个解析过程主要分为以下两个步骤:

分词:将整个代码字符串分割成最小语法单元数组

语法分析:在分词基础上建立分析语法单元之间的关系

JS Parser 是 js 语法解析器,它可以将 js 源码转成 AST,常见的 Parser 有 esprima、traceur、acorn、shift 等。

词法分析

词法分析,也称之为扫描(scanner),简单来说就是调用 next() 方法,一个一个字母的来读取字符,然后与定义好的 JavaScript 关键字符做比较,生成对应的Token。Token 是一个不可分割的最小单元:

例如 var 这三个字符,它只能作为一个整体,语义上不能再被分解,因此它是一个 Token。

词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等。

最终,整个代码将被分割进一个tokens列表(或者说一维数组)。

语法分析

语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。

说了这么多我们来看下 javaScript 代码片段转成 AST 之后是什么样的我们拿一行简单的代码来展示

例子 1

const fn = a => a;

如图从这个 AST 语法树我们就能够很清楚的看出一个代码他的具体含义,并且使用的是什么语法,方法等。

用人话翻译这个图就是:用类型 const 声明变量 fn 指向一个箭头函数表达式,它的参数是 a 函数体也是 a。

例子 2

const fn = a => { let i = 1; return a + i; };

 

我们来看 body 这块:

例子 3

函数调用

function test(){ let a = 1; console.log(a) }

 

主要看 MemberExpression

以上截图均是使用 Acorn 解析。使用 Acorn 的原因是据我了解在 parser 解析中,Acorn 是公认的最快的。并且我们使用的 Webpack 打包工具中 babel 用的也是 Acorn。

上述截图的属性是 AST 的一部分,这个结构包含了很多属性。

VariableDeclaration 变量声明

VariableDeclarator 变量声明的描述

Expression 表达式节点

更多属性展示:

可以去 AST explorer 可以在线看到不同的 parser 解析 js 代码后得到的 AST。

github 上看所有的 ESTree ESTree

关于属性介绍的文档 抽象语法树AST介绍

实战 AST 的运用

题目

通过上面介绍的 console.log AST,下面我们就来完成一个在调用 console.log(xx) 时候给前面加一个函数名,这样用户在打印时候能改方便看到是哪个函数调用的。

举例

// 源代码 function getData() { console.log("data") } // -------------------- // 转化后代码 function getData() { console.log("getData", "data"); }

 

介绍

首先介绍下我们需要使用的工具 Babel

@babel/parser : 将 js 代码 ------->>> AST 抽象语法树;

@babel/traverse 对 AST 节点进行递归遍历;

@babel/types 对具体的 AST 节点进行进行修改;

@babel/generator : AST 抽象语法树 ------->>> 新的 js 代码;

为什么使用 babel ? 主要是比较好用(只对这个比较熟悉��)。

进入 @babel/parser 官网开头就介绍了它是使用的 Acorn 来解析 js 代码成 AST 语法树(说明确实 Acorn 比较好)。

开始码起来

新建文件打开控制台安装需要的包

cnpm i @babel/parser @babel/traverse @babel/types @babel/generator -D

复制代码cnpm i @babel/parser @babel/traverse @babel/types @babel/generator -D

创建 js 文件, 编写大致布局如下 使用 AST

const generator = require("@babel/generator"); const parser = require("@babel/parser"); const traverse = require("@babel/traverse"); const types = require("@babel/types"); function compile(code) { // 1.parse 将代码解析为抽象语法树(AST) const ast = parser.parse(code); // 2,traverse 转换代码 traverse.default(ast, {}); // 3. generator 将 AST 转回成代码 return generator.default(ast, {}, code); } const code = ` function getData() { console.log("data") } `; const newCode = compile(code)

 

使用 node 跑出结果,因为什么都没处理,输出的是原代码,

完善 compile 方法

function compile(code) { // 1.parse const ast = parser.parse(code); // 2,traverse const visitor = { CallExpression(path) { // 拿到 callee 数据 const { callee } = path.node; // 判断是否是调用了 console.log 方法 // 1. 判断是否是成员表达式节点,上面截图有详细介绍 // 2. 判断是否是 console 对象 // 3. 判断对象的属性是否是 log const isConsoleLog = types.isMemberExpression(callee) && callee.object.name === "console" && callee.property.name === "log"; if (isConsoleLog) { // 如果是 console.log 的调用 找到上一个父节点是函数 const funcPath = path.findParent(p => { return p.isFunctionDeclaration(); }); // 取函数的名称 const funcName = funcPath.node.id.name; // 将名称通过 types 来放到函数的参数前面去 path.node.arguments.unshift(types.stringLiteral(funcName)); } } }; // traverse 转换代码 traverse.default(ast, visitor); // 3. generator 将 AST 转回成代码 return generator.default(ast, {}, code); }

 

纯代码看起来比较难理解下面是我将上面的 path.node 写入到文件中给大家看下数据格式。

复制代码{

{ "type": "CallExpression", "start": 24, "end": 43, "loc": { "start": { "line": 3, "column": 2 }, "end": { "line": 3, "column": 21 } }, "callee": { "type": "MemberExpression", "start": 24, "end": 35, "loc": { "start": { "line": 3, "column": 2 }, "end": { "line": 3, "column": 13 } }, "object": { "type": "Identifier", "start": 24, "end": 31, "loc": { "start": { "line": 3, "column": 2 }, "end": { "line": 3, "column": 9 }, "identifierName": "console" }, "name": "console" }, "property": { "type": "Identifier", "start": 32, "end": 35, "loc": { "start": { "line": 3, "column": 10 }, "end": { "line": 3, "column": 13 }, "identifierName": "log" }, "name": "log" }, "computed": false }, "arguments": [ { "type": "StringLiteral", "start": 36, "end": 42, "loc": { "start": { "line": 3, "column": 14 }, "end": { "line": 3, "column": 20 } }, "extra": { "rawValue": "data", "raw": "'data'" }, "value": "data" } ] }

我们将不必要的位置信息(start, end, loc)属性删除,对照数据来看代码将会一目了然

再跑该文件

很好,调用 console.log 方法参数前面增加了函数名,完成!!

为了大家能够方便运行,下面是完整代码

const generator = require("@babel/generator"); const parser = require("@babel/parser"); const traverse = require("@babel/traverse"); const types = require("@babel/types"); const fs = require("fs"); function compile(code) { // 1.parse const ast = parser.parse(code); // 2,traverse const visitor = { CallExpression(path) { const { callee } = path.node; const isConsoleLog = types.isMemberExpression(callee) && callee.object.name === "console" && callee.property.name === "log"; if (isConsoleLog) { const funcPath = path.findParent(p => { return p.isFunctionDeclaration(); }); const funcName = funcPath.node.id.name; fs.writeFileSync("./funcPath.json", JSON.stringify(funcPath.node), err => { if (err) throw err; console.log("写入成功"); }); path.node.arguments.unshift(types.stringLiteral(funcName)); } } }; traverse.default(ast, visitor); // 3. generator return generator.default(ast, {}, code); } const code = ` function getData() { console.log('data') } `; console.log(compile(code).code);

 

看到这里,如果你觉得都没什么问题,相信你对 AST 已经有了很清楚的认识了,并且对 babel 编译代码也有了一定的理解,以后写 webpack 配置也就不会对 babel 那么陌生了。

总结

为了兼容低版本浏览器 我们也通常会使用 webpack 打包编译我们的代码将 ES6 语法降低版本,比如箭头函数变成普通函数。将 const、let 声明改成 var 等等,他都是通过 AST 来完成的,只不过实现的过程比较复杂,精致。不过也都是这三板斧:

js 语法解析成 AST;

修改 AST;

AST 转成 js 语法;

最后

有时间,大家在尝试完成之后也同样可以试试箭头函数转普通函数等一些常用的代码转换,这样可以很好的加深印象。

   
1336 次浏览       24
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程

最新活动计划
数据建模方法与工具 12-16[北京]
基于模型系统仿真与验证 12-14 [讲座]
白盒测试技术与工具实践 12-24[线上]
LLM大模型应用与项目构建 12-26[特惠]
UML和EA进行系统分析设计 12-20[线上]
SysML建模专家 1-16[北京]
 
 
最新文章
.NET Core 3.0 正式公布:新特性详细解读
.NET Core部署中你不了解的框架依赖与独立部署
C# event线程安全
简析 .NET Core 构成体系
C#技术漫谈之垃圾回收机制(GC)
最新课程
.Net应用开发
C#高级开发技术
.NET 架构设计与调试优化
ASP.NET Core Web 开发
ASP.Net MVC框架原理与应用开发
成功案例
航天科工集团子公司 DotNet企业级应用设计与开发
日照港集 .NET Framewor
神华信 .NET单元测试
台达电子 .NET程序设计与开发
神华信息 .NET单元测试