简单玩一下AST(JavaScript)

直奔主题

对于js,AST能干什么?javascript

  • babel将es6转es5
  • mpvue、taro等将js转为小程序
  • 定制插件删除注释、console等

ps: 本文只探讨AST的概念以及使用,编译原理的其余知识不作太多描述vue

工具库

@babel/corejava

  • 用来解析AST以及将AST生成代码

@babel/typesnode

  • 构建新的AST节点

前置知识 - 编译原理概述

毫无疑问js是一个解释型语言,有疑问能够参考这篇文章
因此这里只简单描述一下babel的编译过程(大雾),有兴趣了解编译型语言详细编译过程的能够看这本 《编译原理》es6

和编译器相似,babel 的转译过程也分为三个阶段,这三步具体是:

解析 Parse
将代码解析生成抽象语法树( 即AST ),也就是计算机理解咱们代码的方式(扩展:通常来讲每一个 js 引擎都有本身的 AST,好比熟知的 v8,chrome 浏览器会把 js 源码转换为抽象语法树,再进一步转换为字节码或机器代码),而 babel 则是经过babylon 实现的 。简单来讲就是一个对于 JS 代码的一个编译过程,进行了词法分析与语法分析的过程。chrome

转换 Transform
对于 AST 进行变换一系列的操做,babel 接受获得 AST 并经过 babel-traverse 对其进行遍历,在此过程当中进行添加、更新及移除等操做。typescript

生成 Generate
将变换后的 AST 再转换为 JS 代码, 使用到的模块是 babel-generator小程序

babel-core 模块则是将三者结合使得对外提供的API作了一个简化。segmentfault

生成AST

demo.js是我随便copy来的一段代码浏览器

isLeapYear()

function isLeapYear(year) {
    const cond1 = year % 4 == 0;  //条件1:年份必需要能被4整除
    const cond2 = year % 100 != 0;  //条件2:年份不能是整百数
    const cond3 = year % 400 ==0;  //条件3:年份是400的倍数
    const cond = cond1 && cond2 || cond3;

    console.log(cond)

    if(cond) {
        alert(year + "是闰年");
        return true;
    } else {
        alert(year + "不是闰年");
        return false;
    }
}

如今我要把它转成AST,这里使用@babel/core来解析,它提供了一个parse方法来将代码转化为AST。

parse.ts就是个人解析工具

import * as fs from 'fs'
import * as path from 'path'

import { parse} from '@babel/core'

const js_path = path.resolve(__dirname, '../demo.js')
let code = fs.readFileSync(js_path, {
    encoding: 'utf-8'
})

const js_ast = parse(code)
console.log(js_ast)

能够看到AST结果以下:

clipboard.png

结果太长就不一一解析了,只说type属性,就表示了这一行代码作了什么,VariableDeclaration就表示这是一句声明语句, CallExpression则表明这是一个调用函数的语句

将AST转回代码

@babel/core提供了一个transform方法,输入代码和修改代码的规则,输出修改过的AST,它看起来是这样的:

const ArrowPlugins = {
    visitor: {
        VariableDeclaration(path: NodePath) {
            // ...
        },
        CallExpression(path: NodePath) {
            // ...
        }
    }
}

const d = transform(code, {
    plugins: [
        ArrowPlugins
    ]
})

当命中对应的type时就会走进相应的回调函数,接下来写个小🌰,将alert,console.log以及所有注释都删除,而后将 ==!= 改为 ===!==

完整代码

import * as fs from 'fs'
import * as path from 'path'
import { transform, parse, NodePath } from '@babel/core'
import { VariableDeclaration, CallExpression, MemberExpression, Identifier, BinaryExpression } from '@babel/types'

const js_path = path.resolve(__dirname, '../demo.js')
let code = fs.readFileSync(js_path, {
    encoding: 'utf-8'
})

// const js_ast = parse(code)
// debugger

const ArrowPlugins = {
    visitor: {
        VariableDeclaration(path: NodePath) { // 修改== -> ===
            const node = path.node as VariableDeclaration
            node.declarations.map((item) => {
                const init = item.init as BinaryExpression
                const equalMap = {
                    '==': '===',
                    '!=': '!=='
                }
                init.operator = equalMap[init.operator] || init.operator
            })
            // 删除注释
            delete node.leadingComments
            delete node.trailingComments
        },
        CallExpression(path: NodePath) { // 调用函数
            const node = path.node as CallExpression
            // 删除console.xxx 和 alert
            const memberExpressionCallee = node.callee as MemberExpression
            const identifierCallee = node.callee as Identifier
            const object = memberExpressionCallee.object as Identifier

            if (object && object.name === 'console' || identifierCallee.name === 'alert') {
                path.remove()
            }
            // 删除注释
            delete node.leadingComments
            delete node.trailingComments
        }
    }
}

const d = transform(code, {
    plugins: [
        ArrowPlugins
    ]
})

console.log(d.code)

总结

只是简单地使用了一下@babel提供的方法将代码转成AST,并在树枝上作一些简单的修修改改,最后转成目标代码,若是只是平常使用或者用来本身写babel插件通常是足够了,想要了解更多的编译原理知识须要更系统的学习。
等我看完《编译原理》(大雾),再继续更新系列文章

相关文章
相关标签/搜索