直奔主题

对于js,AST能干什么?

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

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

工具库

@babel/core

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

@babel/types

  • 构建新的AST节点

前置知识 - 编译原理概述

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

  1. 词法分析:
    实际就是从左到右一个字一个字地扫描分解,识别出单词、符号等。例如:
    var num = 1会被分解成var,num,=,1
  2. 语法分析
    将词法分析的结果,根据语法规则转化为一个嵌套的对象,也就是AST(Abstract Syntax Tree 即 抽象语法树
  3. 目标代码产生
    将AST转换为可执行代码

生成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结果如下:

结果太长就不一一解析了,只说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)// debuggerconst 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插件一般是足够了,想要了解更多的编译原理知识需要更系统的学习。
等我看完《编译原理》(大雾),再继续更新系列文章