一个简单的例子看懂抽象语法树的魔力

栏目: JavaScript · 发布时间: 5年前

内容简介:在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。上面这是百度百科的定义,一如既往的让人摸不着头脑。我们总结一下百度百科的定义:

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

上面这是百度百科的定义,一如既往的让人摸不着头脑。

我们总结一下百度百科的定义:

  1. 抽象语法树是一颗树
  2. 树的每个节点都表示了源代码的一种 语法结构

上面这段总结有几个关键词:抽象语法树、树、节点、源代码、语法结构,其中语法结构是比较难理解的,那么什么是语法结构呢?

举几个简单的例子:

//变量声明
var a = 1;
复制代码
//循环
while(true){
    console.log(1);
}
复制代码
//判断
if(true){
    console.log(1);
}
复制代码
//函数声明
function a(){
    console.log(1);
}
复制代码

以上这些例子是js的语法声明(statement),这种声明就可以看成js的语法结构! 也就是说抽象语法树的每个节点都在描述这种结构。

比如说一个节点是变量声明,那么这个节点的子节点都会去描述变量声明的具体内容:变量名是什么,变量是什么类型,变量的初始值是什么等等。

就是这样一个一个的声明,构成了抽象语法树。

代码执行的三个步骤

从js程序到机器可执行的机器码需要经历两个阶段:

  • 语法检查
  • 编译运行

语法检查又分为语法分析和词法分析,所以分成三个步骤就是:

  • 词法分析
  • 语法分析
  • 编译运行

这里先简单介绍下每个阶段都干了什么活:

第一步:词法分析,也叫做扫描scanner。它读取我们的代码,然后把它们按照预定的规则合并成一个个的标识Tokens(type 和 value )。这个阶段,它会移除空白符,注释等。最后,整个代码将被分割进一个Tokens列表(一个一维数组)。

第二步:语法分析,也叫做解析器。它会将词法分析出来的Token数组转化成树形的表达形式。同时,验证语法,语法如果有错的话,抛出语法错误。

第三步:编译阶段,也叫编译器。这个阶段会处理AST,生成机器可执行的机械码。

词法分析

先以一个简单的例子看下token序列长什么样

var a = 1;
复制代码

他的token长这样

一个简单的例子看懂抽象语法树的魔力

其实就是一个一维数组,里面有一些对象用于描述每个单词。

我整理了下常见的type:

  • Keyword (关键词)
  • Identifier (标识符)
  • Punctuator (标点符号)
  • Numberic(数字)
  • String (字符串)
  • Boolean(布尔)
  • Null(空值)

语法分析

词法分析由源代码生成了Token序列,语法分析则是由Token序列生成了抽象语法树。

还是看上一个例子:

var a = 1;
复制代码

他的抽象语法树长这样:

一个简单的例子看懂抽象语法树的魔力

1、先看最外层的三个属性

  • type(表示是一段程序代码)
  • body(代码的具体内容)
  • sourceType(表示语言的种类)

2、再看body里面的具体内容,body是一个数组,这是因为程序可能有多个内容块(statement),每个内容块用一个对象表示。

3、再看每个内容块的内容

  • type(表示这个内容块是干什么的)
  • declarations(乘装变量内容的块,可以看到这个块也是一个数组,因为变量声明可能生命多个,所以一个生命对应一个对象 例如 var a=1,b=2;) kind(关键字)

4、再看declarations里面对象里面的内容

  • type (声明的类型是个变量)
  • id(表示变量名)
  • init(表示为这个变量设置的初值)

上面提到statement,statement有很多类型,比如说变量声明,函数定义,if语句,while循环,等都是一个statement,大家如果想看更多的类型,点击这里。

一个超级简单的例子

好了,说了这么多,终于要写代码了。

这个例子实现的功能:

  • 将源代码中的 == 变成 ===
  • 将源代码中的 var 变成 let
  • 将源代码中的 console注释掉

这个例子用到的工具:

  • Esprima (将源代码转化为ast)
  • Estraverse(遍历语法树)
  • Escodegen(讲语法书反编译为js代码)

初始化一个项目

npm init
复制代码

安装用到的依赖包

npm install esprima estraverse escodegen --save
复制代码

新建index.js入口文件 和 originCode.js 源代码文件

在 originCode.js 中输入要转换的源代码

function fun() {
    var opt = 1;
    console.log(1);
    if (opt == 1) {
        console.log(2);
    }
}
复制代码

在 index.js 中实现我们的功能

let fs  = require('fs');
const esprima = require('esprima');//将JS代码转化为语法树模块
const estraverse = require('estraverse');//JS语法树遍历各节点
const escodegen = require('escodegen');//将JS语法树反编译成js代码模块

/**
* 由源代码得到抽象语法树
*/
function  getAst(jsFile) {
    let jsCode;
    return new Promise((resolve)=>{
        fs.readFile(jsFile, (error, data) => {
            jsCode = data.toString();
            resolve(esprima.parseScript(jsCode));
        });
    });
}

/**
 * 设置全等
 */
function setEqual(node) {
    if (node.operator === '==') {
        node.operator = '===';
    }
}

/**
 * 删除console
 */
function delConsole(node) {
    if (node.type === 'ExpressionStatement' && node.expression.type === 'CallExpression' && node.expression.callee.object.name==='console') {
        node.expression.callee.object.name = '//console';
    }
}

/**
 * 把var变成let
 */
function setLet(node){
    if(node.kind === 'var'){    
        node.kind = 'let';
    }
}

/**
 * 遍历语法树
 */
function travel(ast){
    estraverse.traverse(ast, {
        enter: (node) => {
            setEqual(node);
            setLet(node);
            delConsole(node);
        }
    });
}

/**
* 生成文件
*/
function  writeCode(file,data) {
    fs.writeFile(file,data,(error)=>{
        console.log(error);
    });
}

/**
* 入口函数
*/
function main(){
    let file = './originCode.js';
    let distFile = './distCode.js';
    getAst(file).then(function(jsCode) {
        travel(jsCode);
        
        // 删掉 console , 通过parseScript在生成一变ast去掉注释的内容
        // let distCode = escodegen.generate( esprima.parseScript( escodegen.generate(jsCode)));
        // 注释 console
        let distCode = escodegen.generate(jsCode);
        console.log('distcode',distCode);

        writeCode(distFile,distCode);
    });
}

main();
复制代码

然后运行我们的项目

node index.js
复制代码

distCode.js的内容已经变成我们想要的了

function fun() {
    let opt = 1;
    //console.log(1);
    if (opt === 1) {
        //console.log(2);
    }
}
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序设计语言理论基础

程序设计语言理论基础

米切尔 / 电子工业出版社 / 2006-11 / 68.00元

本书提出了一个框架,用于分析程序设计语言的语法、操作和语义性质,该框架基于称为类型化λ演算的数学系统。λ演算的主要特色是对于函数和其他可计算的值的一种记法,以及一个等式逻辑和用于表达式求值的一组规则。本书中最简单的系统是称为泛代数的一个等式系统,它可以用来公理化和分析通常用于程序设计的许多数据类型。可作为理论计算机科学、软件系统和数学专业的大学本科高年级或者研究生初始学习阶段的教材,同时也适合用于......一起来看看 《程序设计语言理论基础》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具