AST(Abstract Syntax Tree) 抽象语法

449 阅读3分钟

webpack和Lint等很多的工具和库的核心都是通过Abstract Syntax Tree抽象语法树这个概念来实现对代码的检查、分析等操作的

抽象语法树用途

  • 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
    • 如JSLint、JSHint对代码错误或风格的检查,发现一些潜在的错误
    • IDE的错误提示、格式化、高亮、自动补全等等
  • 代码混淆压缩
    • UglifyJS2等
  • 优化变更代码,改变代码结构使达到想要的结构
    • 代码打包工具webpack、rollup等等
    • CommonJS、AMD、CMD、UMD等代码规范之间的转化
    • CoffeeScript、TypeScript、JSX等转化为原生Javascript

抽象语法树定义

工具的原理都是通过JavaScript Parser把代码转化为一颗抽象语法树(AST),这颗树定义了代码的结构,通过操纵这颗树,可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作

Javascript的语法是为了给开发者更好的编程而设计的,但是不适合程序的理解。所以需要转化为AST来使之更适合程序分析,浏览器编译器一般会把源码转化为AST来进行进一步的分析等其他操作。

JavaScript Parser

  • JavaScript Parser,把js源码转化为抽象语法树的解析器。
  • 浏览器会把js源码通过解析器转为抽象语法树,再进一步转化为字节码或直接生成机器码。
  • 每个js引擎都会有自己的抽象语法树格式,Chrome的v8引擎,firefox的SpiderMonkey引擎等等,MDN提供了详细SpiderMonkey AST format的详细说明,算是业界的标准。

常用的JavaScript Parser有:

  • esprima
  • traceur
  • acorn
  • shift

esprima

通过 esprima 把源码转化为AST

AST的可视化工具
esprima
astexplorer

  • 解析语法(js) => 语法树
let esprima = require('esprima');
let code = `function ast(){}`
let ast = esprima.parseScript(code);
console.log(ast)

结果

{
    "type": "Program",
    "body": [
        {
            "type": "FunctionDeclaration",
            "id": {
                "type": "Identifier",
                "name": "ast"
            },
            "params": [],
            "body": {
                "type": "BlockStatement",
                "body": []
            },
            "generator": false,
            "expression": false,
            "async": false
        }
    ],
    "sourceType": "script"
}

estraverse

通过 estraverse 遍历并更新AST

let estraverse = require('estraverse');
estraverse.traverse(ast, {
    enter(node){ //直接用enter
        if (node.type === 'Identifier') {
            node.name = 'testAST'
        }
        console.log('enter', node.type);
    },
    leave(node) {
        console.log('leave', node.type);
    }
});

结果

enter Program
enter FunctionDeclaration
enter Identifier
leave Identifier
enter BlockStatement
leave BlockStatement
leave FunctionDeclaration
leave Program

escodegen

通过 escodegen 将AST重新生成源码

let escodegen = require('escodegen');
let r = escodegen.generate(ast)
console.log(r);

结果

function testAST() {
}

箭头函数插件 (ArrowFunctionPlugin)

  • 通过@babel/core、@babel/types
  • @babel/types
    • 生成语法树
    • 判断类型
let babel = require('@babel/core');
let t = require('@babel/types');

let code = `let sum = (a, b) => {return a + b}`;

let ArrowPlugin = {
    visitor: {
        //path 树的路径
        ArrowFunctionExpression(path) {
            let node = path.node;
            let params = node.params;
            let body = node.body;
            //判断是不是代码块
            if (!t.isBlockStatement(body)) {
                let returnStatement = t.returnStatement(body)
                body = t.blockStatement([returnStatement])
            }
            //生成函数表达式
            let funcs = t.functionExpression(null, params, body, false, false);
            path.replaceWith(funcs)
        }
    }
}


let r = babel.transform(code, {
    plugins: [
        ArrowPlugin
    ]
});
console.log(r.code)

类函数插件(ClassFunctionPlugin)

let babel = require('@babel/core');
let t = require('@babel/types');

let code = `
    class Person {
        constructor(name, age) {
            this.name = name;
        }
        getName(){
            return this.name
        }
    }
`
let ClassPlugin = {
    visitor:{
        ClassDeclaration(path) {
            let node = path.node;
            let className = node.id.name; //函数名必须是个标识符
            className = t.identifier(className);
            
            let classList = node.body.body;
            let funcs = t.functionDeclaration(className, [], t.blockStatement([]), false, false);
            path.replaceWith(funcs);
            
            let es5Funcs = []
            classList.forEach((item, index) => {
                //函数代码体
                let body = classList[index].body;
                if (item.kind === 'constructor') {
                    //如果是构造函数就生成新的函数将默认的空函数替换掉
                    let params = item.params.length ? item.params.map(item => item.name) : [];
                    params = t.identifier(params.toString());
                    funcs = t.functionDeclaration(className, [params], body, false, false);
                }else{
                    let protoObj = t.memberExpression(className, t.identifier('prototype'));
                    let left = t.memberExpression(protoObj, t.identifier(item.key.name));
                  
                    let right = t.functionExpression(null, [], body, false, false);
                    let assign = t.assignmentExpression('=', left, right);
                    //多个原先方法
                    es5Funcs.push(assign)
                }
            })
            if (es5Funcs.length == 0) {
                 path.replaceWith(funcs);
            }else {
                //有原先方法
                es5Funcs.push(funcs);
                //替换n个节点
                path.replaceWithMultiple(es5Funcs)

            }
        }
    }
}

let r = babel.transform(code, {
    plugins: [
        ClassPlugin
    ]
});
console.log(r.code)

Tree Shaking (ImportFunctionPlugin)

//实现模块按需 加载

//import {Button} from 'antd';

//babel-plugin-import


//import {Button, Alter} from 'antd';
//import Button from 'antd/lib/button';
//import Alter from 'antd/lib/alter';


let babel = require('@babel/core');
let t = require('@babel/types');

let code = `import {Button, Alter} from 'antd';`


let ImportPlugin = {
    visitor:{
        ImportDeclaration(path) {
            let node = path.node;
            let source = node.source.value;
            let specifiers = node.specifiers;
            if (!t.isImportDefaultSpecifier(specifiers[0])) {
                specifiers = specifiers.map(specifier => {
                    return t.importDeclaration(
                        [t.importDefaultSpecifier(specifier.local)],
                        t.stringLiteral(`${source}/lib/${specifier.local.name.toLowerCase()}`)
                    )
                });
                path.replaceWithMultiple(specifiers);
            }
        }
    }
}

let r = babel.transform(code, {
    plugins: [
        ImportPlugin
    ]
})
console.log(r.code);