抽象语法树AST以及babel原理

什么是AST?

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

其实意思就是将代码以树的格式展现出来,举个例子你就明白了:
大家可以在这个网站 https://astexplorer.net/ 上输入代码,会被解析为 AST,如图:
在这里插入图片描述
在这里插入图片描述
可以看到,代码的每一个字符都以树的形式展示了出来。

AST的应用场景

常见的场景:

  • IDE的错误提示、代码格式化、代码高亮、代码自动补全等
  • JSLint、JSHint对代码错误或风格的检查等
  • webpack、rollup进行代码打包等
  • CoffeeScript、TypeScript、JSX等转化为原生Javascript、babel把es6转换成es5语法

代码执行的步骤

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

  • 语法检查(源代码 -> 抽象语法树AST)
    • 词法分析(源代码 -> Token序列)
    • 语法分析(Token序列 -> 抽象语法树AST)
  • 编译运行(抽象语法树AST -> 机器码)

1、词法分析

源代码 -> Token序列
将整个代码字符串分割成最小语法单元数组,也就是一个个的标识Tokens(type 和 value )。
https://esprima.org/demo/parse.html#

[
    {
        "type": "Keyword",
        "value": "var"
    },
    {
        "type": "Identifier",
        "value": "a"
    },
    {
        "type": "Punctuator",
        "value": "="
    },
    {
        "type": "Numeric",
        "value": "1"
    },
    {
        "type": "Punctuator",
        "value": ";"
    }
]

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

2、语法分析

Token序列 -> 抽象语法树

{
  // 表示是一段程序代码
  "type": "Program",
  // 代码的具体内容
  "body": [
    {
      // 表示这个内容块是干什么的
      "type": "VariableDeclaration",
      // 装变量内容的块
      "declarations": [
        {
          // 声明的类型是个变量
          "type": "VariableDeclarator",
          // 表示变量名
          "id": {
            "type": "Identifier",
            "name": "a"
          },
          // 表示为这个变量设置的初值
          "init": {
            "type": "Literal",
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "var"
    }
  ],
  // 表示语言的种类
  "sourceType": "script"
}

3、编译运行

这个阶段会处理AST,生成机器可执行的机械码。

举个例子

上面讲了 AST 可以对代码进行转换,那么我们举个例子详细说明一下。

比如我们要实现:

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

这个例子用到的工具:

  • esprima(源代码 -> 抽象语法树AST)
  • estraverse(遍历AST)
  • escodegen(将AST反编译为js代码)

originCode.js 为要转换的源代码

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

index.js 是我们要执行的 node 文件

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);
    }
}

总结

再看一遍这个流程:
源代码 -> 抽象语法树AST -> 机器码/新的js

从上面的这个例子可以看出,我们可以通过修改 AST,从而修改成我们想要的结果,那么像IDE代码高亮、代码混淆压缩、Babel es6转es5、ts转js等等就很好理解了。

babel原理

babel 原理也是 AST,babel 有许多工具可以直接使用,如下:

babel把es6转换成es5语法的原理?

  1. 解析(parse)。将源代码解析变成AST。@babel/parser模块
  2. 转换(transform)。对AST 进行遍历,在此过程中对节点进行添加、更新及移除等操作。因此这是bebel处理代码的核心步骤,是我们的讨论重点,主要使用@babel/traverse和@babel/types模块。
  3. 生成(generate)。将更改后的AST,再变回代码。@babel/generate模块。

举例:将 var 变为 let

const generator = require('@babel/generator');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse');
const transToLet = code => {
  const ast = parser.parse(code);
  // 访问者对象
  const visitor = {
    // 遍历声明表达式
    VariableDeclaration(path) {
      if (path.node.type === 'VariableDeclaration') {
        // 替换
        if (path.node.kind === 'var') {
          path.node.kind = 'let';
        }
      }
    },
  };
  traverse.default(ast, visitor);
  // 生成代码
  const newCode = generator.default(ast, {}, code).code;
  return newCode;
};

const code = `const a = 1
var b = 2
let c = 3`;
transToLet(code);

结果

const a = 1;
let b = 2;
let c = 3;

参考资料

AST规范

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lvan的前端笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值