资讯专栏INFORMATION COLUMN

从现在起-彻底学会 js ast

xiyang / 1461人阅读

摘要:利用抽象语法树可以对你的源代码进行修改优化,甚至可以打造自己的编译工具。


这是一棵树嘛

直奔主题

抽象语法树是js代码另一种结构映射,可以将js拆解成AST,也可以把AST转成源代码。这中间的过程就是我们的用武之地。 利用 抽象语法树(AST) 可以对你的源代码进行修改、优化,甚至可以打造自己的编译工具。其实有点类似babel的功能。

AST高深的狠吓人?

AST很简单,并没有你想象的那样高深。很多地方都把这个技术给夸大了,什么编译原理,抽象语法树 光看这名字就觉得吓人。当然一项技术总归要起个名字,就像给自己的孩子取名字,肯定要起一个高大上,深有寓意的名字。所以,名字只是一个代号。从名字来看就会让很多人望而却步。但是ast超级简单,但是功能超级强大。

我们能用这个技术做很多有意思的东西,只要你能想到的。

本文术道结合,让你感受到ast的有趣和简单,从此爱上ast,还能根据自己的需要打造自己的编译器。

什么是AST?

ast全称是abstract syntax tree,翻译过来叫-抽象语法树。其实这含两个意思,一个是“抽象”,一个是“树”。抽象表示把js代码进行了结构化的转化,转化为一种数据结构。这种数据结构其实就是一个大的json对象,json我们都熟悉,他就像一颗枝繁叶茂的树。

有树根,有树干,有树枝,有树叶.无论多小多大,都是一棵完整的树。

如何生成AST?

你可以大致的想一下如果亲自实现把js代码转换成结构化的数据我们应该怎么做?

有点像小时候拆解自己的玩具,每个零件之间都有着从属关系。

对于如何生成ast,我们可能会想到分析js代码的规则使用字符串处理、正则匹配等方法,如果对简单的代码处理我们是可以实现的。但是如果能够对随意的一段代码进行处理那就需要考虑非常多的情况。具体如何实现咱们不必过于纠结,这也不是重点。

但最终的实现里我们能想到方法基本都会被用到。我们可以简化理解,也就是对js代码经过了一系列的加工处理,变成了一堆零件或者食材(像老妈给我们做的香喷喷的饭菜,但前提是先准备好菜)。

这个拆解的过程可能较为复杂,所以我们需要用现成方法,直接拿过来用就可以了。

所以我们需要用到esprima、UglifyJS等库,做菜的食材有很多种,所以会存在很多这样的三方库,而我们会使用其中一种就可以了。

先使用esprima 种菜,体会一下

种子:

//源代码
function fun(a,b){
  
}

成熟:

{
            "type": "FunctionDeclaration",//函数声明
            "id": {
                "type": "Identifier",//标识符
                "name": "fun" //函数名称
            },
            "params": [//函数参数
                {
                    "type": "Identifier",//参数标识符
                    "name": "a"//参数名称
                },
                {
                    "type": "Identifier",
                    "name": "b"
                }
            ],
            "body": {//函数体
                "type": "BlockStatement",//语句块儿
                "body": []//具体内容为空,因为是空方法
            }
      }
有了AST能做什么?

到这一步你已经可以把js代码转换成一棵结构化的树了,那下一步要做什么呢? 比如在没有树的情况下,你要对代码里的某个代码进行替换。要把所有 console.log给注释掉或者删除,你可能会使用IDE的查找替换或者用node写一个方法,读取文件然后查找替换。

这种方式不够安全也不够科学,稍有不慎就会把代码给搞坏了。

但这个时候你有了结构化代码树,是不是只要对这棵树进行修修剪剪然后把这棵树转换成为js代码就可以了呢?

答案:肯定是可以的。因为树已经发生了变化,修改了树就相当于修改了源码。

怎样操作这棵树呢?我想你应该已经知道了,就是对这json对象进行操作,方法就多了去了,前提是你得有一点点js基础。

又一个问题,怎样把树再转成代码?

脑洞打开,用递归加字符串拼接,这个方法应该是可以的。

但是这棵树不是你生成的,结构特点你并不清楚,成千上万个节点呢?怎么拼接?真要干,那可能得搞得流鼻血。

这就像是食材准备好了,转换成源码的过程就是炒菜的过程。具体的转源码的原理不多说,也不必纠结。使用现成的方法就可以,所以要用到estraverse,escodegen这两个库。

estraverse 可以遍历树的所有节点,省去你对树的递归遍历

escodegen 可以把树再加工转成源代码

过程总结

到这里始终都没有提到任何代码,只是理论了一番,但是相信你已经理解了ast以及ast的作用。然后在述说过程中引出了3个库,有了这三个库就可以对你的js代码进行多样化处理,只要你能想到的。

看图理解整个处理过程:

这个过程简单,清晰,所以说ast简单、有趣、好玩。因为此刻代码可以被你任意的蹂躏了。

实例应用

说的再清楚都不够直观,毕竟都是脑补,不如看代码来的爽快。

这里就拿日常编码中的一些小问题举例,来演示一下AST的使用。

把 == 改为全等 ===

把parsetInt不标准的调用改为标准用法 parseInt(a)-> parseInt(a,10)

这里我使用esprima的官方工具生成了ast,工具地址http://esprima.org/demo/parse...

看下要处理的源码:

//源码
function fun1() {
    console.log("fun1");
}
function fun2(opt) {
    if (opt.status == 1) {
        console.log("1");
    }
    if (opt.status == 2) {
        console.log("2");
    }
}
function fun3(age) {
    if (parseInt(age) >= 18) {
        console.log("ok 你已经成年");
    }
}

转成ast,由于转成树后结构非常大,所以这里我只贴了一部分,你也可以到工具页面自己生成下。

{
    "type": "Program",
    "body": [
        {
            "type": "FunctionDeclaration",
            "id": {
                "type": "Identifier",
                "name": "fun1"
            },
            "params": [],
            "body": {
                "type": "BlockStatement",
                "body": [
                    {
                        "type": "ExpressionStatement",
                        "expression": {
                            "type": "CallExpression",
                            "callee": {
                                "type": "MemberExpression",
                                "computed": false,
                                "object": {
                                    "type": "Identifier",
                                    "name": "console"
                                },
                                "property": {
                                    "type": "Identifier",
                                    "name": "log"
                                }
                            },
                            "arguments": [
                                {
                                    "type": "Literal",
                                    "value": "fun1",
                                    "raw": ""fun1""
                                }
                            ]
                        }
                    }
                ]
            },
            "generator": false,
            "expression": false,
            "async": false
        }
    ]
}

ast看上去结构复杂,盯着仔细看后基本都能看懂。所有的代码都在特定的节点里面。具体的这里就不介绍了,可以到上面的工具地址去观察不同的ast结构。总之这就是一个对象,只要你能对这个对象进行修改、添加、删除即可。

开始实现以上功能
init

//引入工具包
const esprima = require("esprima");//JS语法树模块
const estraverse = require("estraverse");//JS语法树遍历各节点
const escodegen = require("escodegen");//JS语法树反编译模块
//获�取代码ast
const AST = esprima.parseScript(jsCode);

/**
 * 
 * @param {遍历语法树} ast 
 */
function walkIn(ast){
    estraverse.traverse(ast, {
        enter: (node) => {
            toEqual(node);//把 == 改为全等 ===
            setParseint(node); //parseInt(a)-> parseInt(a,10)
        }
    });
}

2.把 == 改为全等 ===

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

把parseInt改成标准调用

/**
 * 把parseint改为标准方法
 * @param {节点} node 
 */
function setParseint(node) {
    //判断节点类型 方法名称,方法的参数的数量,数量为1就增加第二个参数
    if (node.type === "CallExpression" && node.callee.name === "parseInt" && node.arguments.length===1){

        node.arguments.push({//增加参数,其实就是数组操作
            "type": "Literal",
            "value": 10,
            "raw": "10"
        });
    }
}

//生成目标代码
const code = escodegen.generate(ast);
//写入文件.....
//....你懂的

代码不多,需求简单,但已足够能说明整个处理过程以及ast的强大。 ast的节点很多,有些凌乱,送你一首歌【汪峰的无所谓】,操作的时候只要关心你自己的需求就可以,不需要对所有的节点都搞明白。按需处理就可以。

AST技术的应用

虽然平时用不到ast,但又时刻都在使用ast技术。家喻户晓、无人不知的babel,webpack,还有jd taro等都把ast用的淋漓尽致,脱离了ast他们就跪了。

AST这么简单,好没技术含量
AST没有技术含量吗?怎么可能呢,如果真这么认为怕是会被笑掉大牙的。如果仅仅停留在使用层面的话,理解到这步已经基本可以了,只要是你能对这棵树做修剪就可以对源代码做手脚。

另外ast怎样生成的?怎样把ast转换成源码的?这就有点高深了。会使用就像是在山脚下能看到的风景有限,理解了背后原理机制就像是爬上了山顶,别样的风景尽收眼底。不过上不上山看个人兴趣,有兴趣的同学可以去看源码、做研究,这里就不再多说,因为我也不知道。哈哈哈

总结

本文主要介绍了

什么是ast:

ast其实就把js代码进行抽象为一种json结构;

ast的用途:

利用ast可以方便的优化和修改代码,还能打造自己的编译器;

然后通过具体的示例演示了怎样操作ast,最终是希望你能对ast有一个系统全局的认识和理解并能够利用ast打造自己的编译工具。

演示代码下载,欢迎star

https://github.com/bigerfe/fo...

自家观点,欢迎打脸

原创不易,请多鼓励

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101339.html

相关文章

  • 平庸前端码农之蜕变 — AST

    摘要:为什么要谈抽象语法树如果你查看目前任何主流的项目中的,会发现前些年的不计其数的插件诞生。什么是抽象语法树估计很多同学会和图中的喵一样,看完这段官方的定义一脸懵逼。它读取我们的代码,然后把它们按照预定的规则合并成一个个的标识。 前言 首先,先说明下该文章是译文,原文出自《AST for JavaScript developers》。很少花时间特地翻译一篇文章,咬文嚼字是件很累的事情,实在...

    dreamans 评论0 收藏0
  • 【译】小二百行 JavaScript 打造 lambda 演算解释器

    摘要:在开始解析之前,先通过词法分析器运行源码,这会将源码打散成语法中全大写的部分。我们基于每个规则的名称的左侧为其创建一个方法,再来看右侧内容如果是全大写的单词,说明它是一个终止符即一个,词法分析器会用到它。 本文转载自:众成翻译译者:文蔺链接:http://www.zcfy.cc/article/661原文:http://tadeuzagallo.com/blog/writing-a-l...

    KitorinZero 评论0 收藏0
  • babel的初步了解

    摘要:表示的是在严格模式下解析并且允许模块定义即能识别和语法识别不了。 前段时间开始研究ast,然后慢慢的顺便把babel都研究了,至于ast稍后的时间会写一篇介绍性博客专门介绍ast,本博客先介绍一下babel的基本知识点。 背景: 由于现在前端出现了很多非es5的语法,如jsx,.vue,ts等等的格式和写法,如果要在浏览器的设备上识别并执行,需要额外将这些非传统格式的语法转成传统的es...

    _Dreams 评论0 收藏0
  • 彻底搞懂 PHP 变量结构体,多数文章观点不准确

    摘要:中的用于类型整型和资源类型用于浮点类型用于字符串用于数组用于对象用于常量表达式才有多数文章,在提到变量结构体的时候,都提到,实际上这个论述并不准确,在为时,这个结果是正确的。主要看中的,是两个,这个永远是个字节,所以,因此。 PHP5 中的 zval // 1. zval typedef struct _zval_struct { zvalue_value value; ...

    mtunique 评论0 收藏0
  • 阿里云前端周刊 - 第 31 期

    摘要:发布按照官方发布计划,的发布意味着进入阶段,彻底退出舞台,的还有半年结束。为了应对这个挑战,美团点评境外度假前端研发团队自年月起启动了面向端用户的赫尔墨斯项目。前端技术越来越复杂,有不低的技术门槛。 推荐 1. 利用 Dawn 工程化工具实践 MobX 数据流管理方案 https://zhuanlan.zhihu.com/p/... 项目在最初应用 MobX 时,对较为复杂的多人协作项...

    madthumb 评论0 收藏0

发表评论

0条评论

xiyang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<