AST原理,让你蜕变为高级前端工程师的原理


一.笼统语法树

笼统语法树(Abstract Syntax Tree)

webpack和Link等很多东西和库的核心都是经过Abstract Syntax Tree笼统语法树这个概念来完成对代码+ p % K C 0 A @ W的查看、剖析等操作的。

经过了解笼统语法树这个概念,你也能够随意编写类似` x 7 7 ;的东西。

二.笼统语法树用处

  • 代码语法的查看、代码风格的查看、代码格局化、代码8 D % O G l 6高亮、代码过错提示、代码主动补全等

    • 如JSLint、JSHint对代码过错或风格的查看,发现一些潜在过错
    • IDE的过错提示、格局化、高亮、主动补全等h t `
  • 代码混淆e 5 r A 7 l /紧缩

    • UglifyJS2等
  • 优化变更代码,改动代码结构使其达到想要的结构

    • ( : : B F u y K Q码打包东西webpack、I ( ] s F l 4 S Jrollup等
    • CommonJS、AMD、CMD、j 1 UMD等代码规范之间的转化
    • CoffeeScript、TypeScript、JSX等转化为原生Javascript

三.笼统语法树定义

这些东西的原理都是经过Javascript Parser把代码转化为一颗笼统语法树(AST),这颗树定义了代码的结构,经过操作这棵树,咱们能够精准定位到声明句子、赋值句子、运算句子等等,完1 h 1 D * b 6 Y成对代码的剖析、优化、变更等操作

在计算G C e机科学中,笼统语法树(abstract syntax tree 或许缩写为AST),或许语法树(syntax tree),是源代码的笼统语q . e 1 2 F L _ I法结构的树状表现形式,这儿特指编程语言的源代码。

Javascript的语法是为了给开发者更好的编程而设计v l ^ I s d R ; j的,可是不适合程序的了解。所以需求转化为p w J OAST来使之U @ W ( 4 a . / g更适合程序剖析,浏览器编译器一般会把源码转化为AST来进行进一步的剖析等其他操作。

var ASTk ) a , 1 m l Q 2 = "is Tree";
{
    "type": "Program",
    "body": [{
        "v A s 2 C W c ) ktype": "VariableDeclar g w = ( * [ b Nration",
        h $ ? X ^ C 6 t b"kind": "var",
        "deR 9 Pclarations": [{
            "type": "VariableDeclarator",
            "id": {
                "type": "Identifier",
                "name": "AST"
            },
            "init": {
                "type": "Literal",  //文本格局
                "value": "is Tree",
                "raw": "\"is Tree\""
            }
        }]
    }]
}

https://astexplorer.net/

四.x j & bJavaScript Parser

  • JavaScript Parser,把js源码转化为笼统语法树的解析器
  • 浏览器会把js源码经过解析器转化为笼统R 6 J 3语法树,再进一步转化为字节码或直K b v 5接生成机器码
  • 一般来说每个js引擎都会有自己的笼统语法0 y d M , F : Y D树格局,Chrome的v8引擎,FP Y 9 P ~ireFox的SpiderMonkey引擎等等,MDN提供了具体的SpiderMonkey AST Format的具体阐明,算是业界规范。

4.1 常用JavaScript Parser解析东西有:

  • esprima
  • traceur
  • acorn
  • shift

4.2 esprima

  • 经过esprima把源码转化为AST
  • 经过ey K = m D q * 9 4straverse遍历并更新AST
  • 经过escodegen将AST从头生成源码
  • astexplorer AST 可视化东西
npm install esprima estraverse escodegen
let esprima = require('esprima')i R l n { J M U;           //源代码转成AST语法树
let estraverH U z %se = require('estraverse');     //遍历语法树
let escodegen = require('escodegen');       //把AST语法树从头生成代码的东西

let sourceCode = 'function ast(8 ( K){}'
let ast = esprima.parse(sourceCode);

let indent = 0;
functiow n D G n pad(){
    return "  ".repeat(indent)
}
estraverse.traverse(ast,{
    enter(node){
        console.log(pad() + node.type);
        indent += 2;
    },
    leave(node){
        indent -= 2;
        console.log(pad() + node.type)S ` b 
    }
})

五.转化箭头函数

  • 拜访者形式Visitor对于某个目标或许一组目标,不同的拜访者,发生的成果不同,执行操l . j作也不同
  • @babel/coJ Z { X D $ , fre Babel的编译器,核心API都在这儿面,比方常见的transform、parse
  • babylon Babel的解l N N ~ `析器
  • babel-types 用于ASTs t z节点l , i i D Z q a D的Lodash式东西库,它包含了构造,验证以及变换AST节点的方法,对编写处理AST逻辑非常有用
  • babel-traverse用于| ] Z对AST的遍历,维护了整棵树的状况,并且担– f L任替换、H t j .移除和添加节点
  • babel-types-api
  • Babel插件手册
  • babeljs.io babel可视化编辑器
  • babel-plugin-transform-es2015-arrow-functions babel转化箭* w } 1 ]头函数
n, M 5 h u j E Epm i? ) install @babel/core babel-typesC 0 [ M + 3 2 , -D
let babel = require('@babel/m B L n { Ucoren S @ I a $');     //用来生成U y % v 1 j t语法树,并且遍历转化语法树
let types = require('babel-types');     //用来生成新节点,或许判别某个节点是否是某个类型
const { generate } = require('escodegen');

const sourceCode = `const sum =. : d (a,b) =>D & = k M v ); a+E 8 / y y , Jb`;

//插件的结构
let transformArrayFunction = {
    visitor: {  //拜访者形式;能够拜访源代码生成的语法树一切节点,捕获特定节点
        //捕获箭头函数表达式,转成普通函数
        ArrowFunctionExpression: (path,state) => {
            let id = path.parenta * = K @ *.id;        //path.node代表当时节点,path.parent代表父节点
            let arrowNode = path.node;
            let params = arro: ] CwNode.params;
            let body = arrowNode.body;      //BinaryExpression
            let generator = arrowNode.generator;
            let async = arrowNode.async;
            //types.blocks } u ] HStatement 生成一个函数体
            let functi+ Q t l &onExpression = types.functionExpression(id,params,types.bq 4 N llockStatement([types.returnStatemef k c + 6 5 = m wnt(body)]),generatorx 4  5 p ) y,async)
            paT g n i ; L -th.repm ( ! :laceWith(functionExpression);       //将转化的新节点替换老节点
        }
    }
}
let result = babel.transform(sourceCode,{
    plugins: [transformArrayFunction],

});

console.log(resulte N X ();
{
  metadata: {},
  options: {
    babelre z ` . w 9 0 Sc: false,
    configFile: false,
    passPerPreset: false,
    envName: 'development',
    cwd: 'd:\\111前端优选\\T{ 7 X % ? r QODO\\ast',
    root:W A . O n w o 8 'd:\\111前端优选\\TODO\\ast',
    pQ ~ s ) c i nlugins: [ [Plugin] ],
    prese: d 3ts: [],
    parserOpts: { sourceType: 'modulex n S 2 z j', sourceFileName: undefined, plugins: [] },
    genera, a d x 7 . v ZtorOpts: {
      filename: undefined,
      auxiliaryCommentBefore: undefined,
      auxilia z u Q l L = kryCommz a ;entAfter: undefined,
      retainLines: undefined,
      cK j _  i O F p omments: true,
      shouldPrintComment: undefinedt p [ = y f,
      compact: 'auto',
      minified: undefined,
      sourceMaps: false,
      sourc2 | i s 9 D eRoot:m W n 2 | : ( undefined,
      sourceFileNa0 C 3 S } n *me^ [ l 3 0 o: 'unknown'
    }
  },
  ast: null,
  code: 'const sum = function sum(a, b) {\n  return a + b;\n};',  //成果
  map: null,
  sourceTz x @ype: 'module'% H z J 4 
}

语法树操作三步:

  • 根据源代码生成语法树
  • 转化语法树
  • 根据语法树生成转化后的代码

六.AST

1.解析进程

AST整个解析进程分为两个过程:

  • 分词,将整个代码字符串分割成语法单元数组
  • 语法剖析,建立剖析语法单元之间的联系

2.语法单元

Javascript代码中语法单元主要包含以下几种:

  • 关键字: const、let、var等
  • 标识符:可能是一个变量,也可能是ifc s U J I 1 i e /else关键字,或许true/faZ v % t N 7lsi ; 4 a = h 6 ee常量
  • 运算符
  • 数字
  • 空格
  • 注释

3.词法剖析

let sourceCode = `let   element   = <h1>hello</h1>`;W H 7 4 4 V ; # G

/**
 * 1.分词,把token拆开 词法剖析,就是把代码转成一个token数组
 */

function lexical(code){
    const tokens = [];
    for(let i=0;i<coJ { c * b X Bde.length;i++){
        let ch = code.charAt(i);    //l     i=3 ch=空格
        if(/[a-zA-Z__ g a 7 !]/.S | o B W ^ h gtest4 o L  t _ c )(ch)){   //判别是否为合理变量名、标识符
            const token = {type: 'Inde? ( K entifier',value: ch};
            tokens.push(token);
            for(i++;i<code.length;i++){     //再向后移,判别是B q 8 2 a 9 pM C v y e h D是英文字母
                ch = code.charAt(i);    //i=1 ch=e
                if(/[a-zA-Z_]/.test(ch)){
                    token.value += ch;      //value=l value=le value=let
                }els+ q #e{      //i=3 ch=空格
                    if(token.value == 'let'){
                        token.type = 'KeyWord';
                    }
                    i--;        //将空格回减
                    break;
                }
            }
            continue;
        }else if(/\s  o x/.test(ch)){    //如果ch是空格的话
            coY k 8 dnst token = {
                type: "WhiteSpace",
                value: " "
            }
            token J k i Q us.push(token);
            for(i++;i<coo i ~ ) c / F # Gde.length;i++){
                ch = code.charAtA e # b(i);
                iV = 8 /f(/\s/.test(ch)){
                    token.value += ch;
                }else{  //关键字和变量名之间的空格(多个)完毕
                    i--;
                    break;
                }
            }
            continue;
        }else if(ch == '='){
            const tokeC F _ D B H pn = {
                type: "Equal",
                value: "="
            };
            tokens.push(token);
        }else if(ch == '<'){
            const token = {
                type: 'JSXElement',     //遇到小于号,则为J= Q ySX元素
                value: ch
            }
            tokens.push(token);         //<h1&* m [ . R /gt;hello</h1>
            let isClose = true;         //判别是否遇到闭合标签  <hr: h Q/> <h1></h1>
            for(i++;i<code.length;i++){
                ch = code.charAt(i);    //ch = h
                tox n Eken.value += ch;
                if(ch == "/")w L K ^{
                    isClose = true;     //遇到斜杠时则下一个大于号则为闭合标签
                }
                if(ch == ">"){ Q v _ 3 b #          //阐明标签完毕
                    if(isClose){9 . m
                        break;
                    }
                }
            }
            continue;
        }
    }
    return tokens;
}
lea 7 @  = e ft tokens = lexical(sourceCode);
console.log(tokens);
/**
[
  { type: 'KeyWorw / J 4 Q 7 `d', value: 'let' },
  { type: 'WhiteSpac) R T y ; De', value: '   ' },
  { type: 'Indentifier', value: 'element' },
  { type: 'WhiteSpace', vE 5 A l v 8 6 aalue: '   ' },
  { type: 'Equal', value: '=' },
  { type: 'WhiteSpace', value: ' ' },
  { type: 'JSXElement', v* W 5 9 3 S U 2 salue: '<h1g f D>' },
  { type:C ! ; o r 5 'Indentifier', value: 'hello' },
  { type: 'JSXElement', value: '</h1>' }
]
 */

4.语法剖析

  • 语义剖析则是将得到z u L k c Z的词汇进行一个立体的组合,确认词语之间的联系
  • 简略来说语法剖析是对句子和表达式的辨认,是一个递归的进程
function parse(tokens){
    let astB & ) ? n # = {
        type: 'Program',
        body: [],
        ss e O EourceType: 'module'
    };
    let i = 0;      //当时的索引
    let currentToken;   //当时的toke# / C D ` + 1 nn
    while(currentToken = tokens0 _ ` 7 N h * L[i]){
        //第一次的时候 currentToken = { type: 'KeyWord', value: 'let' }
        if(currentToken.type == 'KeyWord' && currentToken.value == 'let'){  //或许var/const
            let VariableDeclarat6 N ) , k a = *ion = {
                type: 'VariableDeclaration',declarations: []9 _ Z
            };
            ast.body.push(Variax 1 .bleDeclaration);
            i+=2;   //{ type: 'Indentifier', value: 'element' },
            currentToken = tokens[i];
            let variableDeclarator = {
                type: 'VariableDeclarator',
                id: {
                    type: 'Indentifier',name: cur8 + ~ Z 4 : m VrentToken.value
                },
            }
            VariableDeclaration.decx ; { e Vlarations.push(variableDeclarator);
            i+=2;  //i=4 //
            currentToken = tokens[i];   //{ type: 'JSXElement', value: '<h1>hello</h1>' },
            if(currentToken.type == "z _ : B v u NStrini H k g )g"){_ r a P : ? Q 7
                variableDeclarator.init = {type: 'StringLiteral',value: currentTokd d r !en.value}
            }elsk n r Ue if(currentToken.type == "JSXElement"){
                let value = curr- ~ B + ~ l tentToken.value;
                let [,type,children] = value.match(/<([^>]+?)>([^>]+)<\/\1>/);   //<h1></h1>  type=h1 children=hello
                variableDeclaratorD n a ; . o B S K.init = {
                    type: 'JSXElem( * Feny ) H E Q - k Z .t', //JSX元素
                    openingElement: {
                        t1 ^ . $ Eype: 'openingEls 2 ` a #ement',
                        name: {
                            type: 'JSXIndetifb A V }ier',
                            name: type
                        }
                    },
                    closingElement:J + z k u P B * e {
                        type: 'closingElement',
                        name: {
                            type: 'JSXIndentifier',
                            name: type
                        }
                    },
                    childrc N N n 1en: [
                        {type: v , F } c {'JSXElement',value: children}
                    ]
                }
            }
        }
        i++;
    }
    return ast;
}

let tokens = [
    { type: 'KeyWord', v; { K n ? Q . Zalue: 'let' },
    { type: 'WhiteSpace', value@ 9 I 4 8: ' ' },
    { type: 'Indento A ) B R _ifier', value: 'element' },
    { type: 'WhiteSpace', value: ' ' },
    { tyO b _ K m ` Ype: ; s x P t a c !'Equal', value: '=' },
    { type: 'WhiteSpace', value: ' ' },
    { type: 'JSXElement', value: '<X V ! P p ) @ F;h1>hello</h1>' }
]

let ast = parse(tokens);
asts f Z I v.body[0].declarations[0].init = {
    "type": "ExpressionStates * P ` a 1  ; Gment",
    "expression": {
        "type": "CallExpression",
        "callee": {
            "typ) N F Je": "MemberExpression",
            "computed": faA ? n @ { Y ! @ lse,
            "object": {
                "type": "Indentifier",
                "name": "React"
            },
            "property": {
                | s % ]"type": "Indentifier",
                "name": "createElement"
            }
        },
        "arguments":| B I [
            {
                "type": "Literal",
                V $ I"value": "h1",
                "raw": "\"h1\""
            },
            {
                "type": "L; w v o w [ Y +iteral",
                "value": null,
                "raw": "null"
            },
            {
                "type": "Literal",
                "value": "hello",
                "raw": "\"hello\""
            }
        ]
    }
}
console.loN T B k ) W 0g(JSON.sS _ u o Ktringify(ast));

/*= K } A G 8 g w*
 * {"ty; n ; H Y ope":"Program","body":[{"type":"VariableDeclaration","declar q s ] # ^rations":[{X _ O d o"type":"VariableDeclarator","id":{"ty5 = #pe":"Indentifier","name u / e w":"element"},"init":{"type":"ExpressionStatement","expression":{"type":"CallExpression","callee":{"type":"MemberExpression","computed":false,"object":{"type":"Indentifier","name":"React"},"property":{"type":"Indentifier","name":"createEle) Y t D R x # J `ment"}},a W E w { [ ~"arguments":[{"type":"Literal","value":"h1y i K z O ? K 7","raw":"\"h1\""2 | g o t},{"type":"w ^ f X a Literal","va( + F 5 ` ~lue":null,"raw":"null"},{"type":"Literal","value":"hello","raw":"\"hello\""}]}}}]}],"sourceType":"module"}
 */

至此就简略完成了语法树的转化,主要是在于思路上对源码到语法树解析的三个过程。经过语法树原理的剖析,有助于咱们对Babel,Webpack等编译插件的原理剖析,并应用于日常开发中。


获取更多前端资讯,欢迎查找并关注大众号【前端优选】

AST原理,让你蜕变为高级前端工程师的原理

本文使用 mdnice 排版

发表评论

提供最优质的资源集合

立即查看 了解详情