簡單玩一下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插件通常是足夠了,想要了解更多的編譯原理知識須要更系統的學習。
等我看完《編譯原理》(大霧),再繼續更新系列文章

相關文章
相關標籤/搜索