天天看點

使用Acorn來解析JavaScript

使用Acorn來解析JavaScript

talk

因為最近工作上有需要使用解析 javascript

的代碼,大部分情況使用正規表達式比對就可以處理,但是一旦依賴于代碼上下文的内容時,正則或者簡單的字元解析就很力不從心了,這個時候需要一個語言解析器來擷取整一個

ast(abstract syntax tree)。

然後我找到了多個使用 javascript 編寫的 javascript 解析器:

esprima

acorn

uglifyjs 2

shift

從送出記錄來看,維護情況都蠻好的,es 各種發展的特性都跟得上,我分别都簡單了解了一下,聊聊他們的一些情況。

<!--more-->

esprima 是很經典的一個解析器,acorn 在它之後誕生,都是幾年前的事情了。按照 acorn

作者的說法,當時造這個輪子更多隻是好玩,速度可以和 esprima 媲美,但是實作代碼更少。其中比較關鍵的點是這兩個解析器出來的 ast

結果(對,隻是 ast,tokens 不一樣)都是符合 the estree spec 規範(這是 mozilla 的工程師給出的

spidermonkey 引擎輸出的 javascript ast 的規範文檔,也可以參考:spidermonkey in

mdn)的,也就是得到的結果在很大部分上是相容的。

現在很出名的 webpack 解析代碼時用的也是 acorn。

至于 uglify,很出名的一個 javascript 代碼壓縮器,其實它自帶了一個代碼解析器,也可以輸出 ast,但是它的功能更多還是用于壓縮代碼,如果拿來解析代碼感覺不夠純粹。

shift 這個沒做多少了解,隻知道他定義了自己的一套 ast 規範。

esprima 官網上有一個性能測試,我在 chrome 上跑的結果如下:

可見,acorn 的性能很不錯,而且還有一個 estree 的規範呢(規範很重要,我個人覺得遵循通用的規範是代碼複用的重要基礎),是以我就直接選用 acorn 來做代碼解析了。

圖中做性能對比的還有 google 的 traceur,它更多是一個 es6 to es5 的 compiler,于我們想要找的解析器定位不符。

下面進入正題,如何使用 acorn 來解析 javascript。

api

解析器的 api 都是很簡單的:

const ast = acorn.parse(code, options) 

acorn 的配置項蠻多的,裡邊還包括了一些事件可以設定回調函數。我們挑幾個比較重要的講下:

ecmaversion

字面意義,很好了解,就是設定你要解析的 javascript 的 ecma 版本。預設是 es7。

sourcetype

這個配置項有兩個值:module 和 script,預設是 script。

主要是嚴格模式和 import/export 的差別。es6 中的子產品是嚴格模式,也就是你無須添加 use strict。我們通常浏覽器中使用的 script 是沒有 import/export 文法的。

是以,選擇了 script 則出現 import/export 會報錯,可以使用嚴格模式聲明,選擇了 module,則不用嚴格模式聲明,可以使用import/export 文法。

locations

預設值是 false,設定為 true 之後會在 ast 的節點中攜帶多一個 loc 對象來表示目前的開始和結束的行數和列數。

oncomment

傳入一個回調函數,每當解析到代碼中的注釋時會觸發,可以擷取當年注釋内容,參數清單是:[block, text, start, end]。

block 表示是否是塊注釋,text 是注釋内容,start 和 end 是注釋開始和結束的位置。

上邊提及的 espree 需要 esprima 的 attachcomment 的配置項,設定為 true 後,esprima

會在代碼解析結果的節點中攜帶注釋相關資訊(trailingcomments 和 leadingcomments)。espree 則是利用

acorn 的 oncomment 配置來實作這個 esprima 特性的相容。

解析器通常還會有一個擷取詞法分析結果的接口:

const tokens = [...acorn.tokenizer(code, options)] 

tokenizer 方法的第二個參數也能夠配置 locations。

至于 acorn 解析的 ast 和 token 的内容我們接下來詳述。

token

我找了半天,沒找到關于 token 資料結構的詳細介紹,隻能自己動手來看一下了。

我用來測試解析的代碼是:

import "hello.js" 

var a = 2; 

// test 

function name() { console.log(arguments); }  

解析出來的 token 數組是一個個類似這樣的對象:

token { 

    type: 

     tokentype { 

       label: 'import', 

       keyword: 'import', 

       beforeexpr: false, 

       startsexpr: false, 

       isloop: false, 

       isassign: false, 

       prefix: false, 

       postfix: false, 

       binop: null, 

       updatecontext: null }, 

    value: 'import', 

    start: 5, 

    end: 11 },  

看上去其實很好了解對不對,在 type 對應的對象中,label 表示目前辨別的一個類型,keyword 就是關鍵詞,像例子中的import,或者 function 之類的。

value 則是目前辨別的值,start/end 分别是開始和結束的位置。

通常我們需要關注的就是 label/keyword/value 這些了。其他的詳細可以參考源碼:tokentype.js。

the estree spec

這一部分是重頭戲,因為實際上我需要的還是解析出來的 ast。最原滋原味的内容來自于:the estree spec,我隻是閱讀了之後的搬運工。

提供了标準文檔的好處是,很多東西有迹可循,這裡還有一個工具,用于把滿足 estree 标準的 ast 轉換為 esmascript 代碼:escodegen。

好吧,回到正題,我們先來看一下 es5 的部分,可以在 esprima: parser 這個頁面測試各種代碼的解析結果。

符合這個規範的解析出來的 ast 節點用 node 對象來辨別,node 對象應該符合這樣的接口:

interface node { 

    type: string; 

    loc: sourcelocation | null; 

}  

type 字段表示不同的節點類型,下邊會再講一下各個類型的情況,分别對應了 javascript 中的什麼文法。

loc 字段表示源碼的位置資訊,如果沒有相關資訊的話為 null,否則是一個對象,包含了開始和結束的位置。接口如下:

interface sourcelocation { 

    source: string | null; 

    start: position; 

    end: position; 

這裡的 position 對象包含了行和列的資訊,行從 1 開始,列從 0 開始:

interface position { 

    line: number; // >= 1 

    column: number; // >= 0 

好了,基礎部分就是這樣,接下來看各種類型的節點,順帶溫習一下 javascript 文法的一些東西吧。對于這裡每一部分的内容,會簡單談一下,但不會展開(内容不少),對 javascript 了解的人很容易就明白的。

我覺得看完就像把 javascript 的基礎文法整理了一遍。

identifier

辨別符,我覺得應該是這麼叫的,就是我們寫 js 時自定義的名稱,如變量名,函數名,屬性名,都歸為辨別符。相應的接口是這樣的:

interface identifier <: expression, pattern { 

    type: "identifier"; 

    name: string; 

一個辨別符可能是一個表達式,或者是解構的模式(es6 中的解構文法)。我們等會會看到 expression 和 pattern 相關的内容的。

literal

字面量,這裡不是指 [] 或者 {} 這些,而是本身語義就代表了一個值的字面量,如 1,“hello”, true 這些,還有正規表達式(有一個擴充的 node 來表示正規表達式),如 /d?/。我們看一下文檔的定義:

interface literal <: expression { 

    type: "literal"; 

    value: string | boolean | null | number | regexp; 

value 這裡即對應了字面量的值,我們可以看出字面量值的類型,字元串,布爾,數值,null 和正則。

regexpliteral

這個針對正則字面量的,為了更好地來解析正規表達式的内容,添加多一個 regex 字段,裡邊會包括正則本身,以及正則的flags。

interface regexpliteral <: literal { 

  regex: { 

    pattern: string; 

    flags: string; 

  }; 

programs

一般這個是作為跟節點的,即代表了一棵完整的程式代碼樹。

interface program <: node { 

    type: "program"; 

    body: [ statement ]; 

body 屬性是一個數組,包含了多個 statement(即語句)節點。

functions

函數聲明或者函數表達式節點。

interface function <: node { 

    id: identifier | null; 

    params: [ pattern ]; 

    body: blockstatement; 

id 是函數名,params 屬性是一個數組,表示函數的參數。body 是一個塊語句。

有一個值得留意的點是,你在測試過程中,是不會找到 type: "function" 的節點的,但是你可以找到 type:

"functiondeclaration" 和 type:

"functionexpression",因為函數要麼以聲明語句出現,要麼以函數表達式出現,都是節點類型的組合類型,後邊會再提及

functiondeclaration 和 functionexpression 的相關内容。

這讓人感覺這個文檔規劃得蠻細緻的,函數名,參數和函數塊是屬于函數部分的内容,而聲明或者表達式則有它自己需要的東西。

statement

語句節點沒什麼特别的,它隻是一個節點,一種區分,但是語句有很多種,下邊會詳述。

interface statement <: node { } 

expressionstatement

表達式語句節點,a = a + 1 或者 a++ 裡邊會有一個 expression 屬性指向一個表達式節點對象(後邊會提及表達式)。

interface expressionstatement <: statement { 

    type: "expressionstatement"; 

    expression: expression; 

blockstatement

塊語句節點,舉個例子:if (...) { // 這裡是塊語句的内容 },塊裡邊可以包含多個其他的語句,是以有一個 body 屬性,是一個數組,表示了塊裡邊的多個語句。

interface blockstatement <: statement { 

    type: "blockstatement"; 

emptystatement

一個空的語句節點,沒有執行任何有用的代碼,例如一個單獨的分号 ;

interface emptystatement  <: statement { 

    type: "emptystatement "; 

debuggerstatement

debugger,就是表示這個,沒有其他了。

interface debuggerstatement <: statement { 

    type: "debuggerstatement"; 

withstatement

with 語句節點,裡邊有兩個特别的屬性,object 表示 with 要使用的那個對象(可以是一個表達式),body 則是對應 with 後邊要執行的語句,一般會是一個塊語句。

interface withstatement <: statement { 

    type: "withstatement"; 

    object: expression; 

    body: statement; 

下邊是控制流的語句:

returnstatement

傳回語句節點,argument 屬性是一個表達式,代表傳回的内容。

interface returnstatement <: statement { 

    type: "returnstatement"; 

    argument: expression | null; 

labeledstatement

label 語句,平時可能會比較少接觸到,舉個例子:

loop: for(let i = 0; i < len; i++) { 

    // ... 

    for (let j = 0; j < min; j++) { 

        // ... 

        break loop; 

    } 

這裡的 loop 就是一個 label 了,我們可以在循環嵌套中使用 break loop 來指定跳出哪個循環。是以這裡的 label 語句指的就是loop: ... 這個。

一個 label 語句節點會有兩個屬性,一個 label 屬性表示 label 的名稱,另外一個 body 屬性指向對應的語句,通常是一個循環語句或者 switch 語句。

interface labeledstatement <: statement { 

    type: "labeledstatement"; 

    label: identifier; 

breakstatement

break 語句節點,會有一個 label 屬性表示需要的 label 名稱,當不需要 label 的時候(通常都不需要),便是 null。

interface breakstatement <: statement { 

    type: "breakstatement"; 

    label: identifier | null; 

continuestatement

continue 語句節點,和 break 類似。

interface continuestatement <: statement { 

    type: "continuestatement"; 

下邊是條件語句:

ifstatement

if 語句節點,很常見,會帶有三個屬性,test 屬性表示 if (...) 括号中的表達式。

consequent 屬性是表示條件為 true 時的執行語句,通常會是一個塊語句。

alternate 屬性則是用來表示 else 後跟随的語句節點,通常也會是塊語句,但也可以又是一個 if 語句節點,即類似這樣的結構:

if (a) { //... } else if (b) { // ... }。

alternate 當然也可以為 null。

interface ifstatement <: statement { 

    type: "ifstatement"; 

    test: expression; 

    consequent: statement; 

    alternate: statement | null; 

switchstatement

switch 語句節點,有兩個屬性,discriminant 屬性表示 switch 語句後緊随的表達式,通常會是一個變量,cases 屬性是一個case 節點的數組,用來表示各個 case 語句。

interface switchstatement <: statement { 

    type: "switchstatement"; 

    discriminant: expression; 

    cases: [ switchcase ]; 

switchcase

switch 的 case 節點。test 屬性代表這個 case 的判斷表達式,consequent 則是這個 case 的執行語句。

當 test 屬性是 null 時,則是表示 default 這個 case 節點。

interface switchcase <: node { 

    type: "switchcase"; 

    test: expression | null; 

    consequent: [ statement ]; 

下邊是異常相關的語句:

throwstatement

throw 語句節點,argument 屬性用以表示 throw 後邊緊跟的表達式。

interface throwstatement <: statement { 

    type: "throwstatement"; 

    argument: expression; 

trystatement

try 語句節點,block 屬性表示 try 的執行語句,通常是一個塊語句。

hanlder 屬性是指 catch 節點,finalizer 是指 finally 語句節點,當 hanlder 為 null 時,finalizer 必須是一個塊語句節點。

interface trystatement <: statement { 

    type: "trystatement"; 

    block: blockstatement; 

    handler: catchclause | null; 

    finalizer: blockstatement | null; 

catchclause

catch 節點,param 用以表示 catch 後的參數,body 則表示 catch 後的執行語句,通常是一個塊語句。

interface catchclause <: node { 

    type: "catchclause"; 

    param: pattern; 

下邊是循環語句:

whilestatement

while 語句節點,test 表示括号中的表達式,body 是表示要循環執行的語句。

interface whilestatement <: statement { 

    type: "whilestatement"; 

dowhilestatement

do/while 語句節點,和 while 語句類似。

interface dowhilestatement <: statement { 

    type: "dowhilestatement"; 

forstatement

for 循環語句節點,屬性 init/test/update 分别表示了 for 語句括号中的三個表達式,初始化值,循環判斷條件,每次循環執行的變量更新語句(init 可以是變量聲明或者表達式)。這三個屬性都可以為 null,即 for(;;){}。

body 屬性用以表示要循環執行的語句。

interface forstatement <: statement { 

    type: "forstatement"; 

    init: variabledeclaration | expression | null; 

    update: expression | null; 

forinstatement

for/in 語句節點,left 和 right 屬性分别表示在 in 關鍵詞左右的語句(左側可以是一個變量聲明或者表達式)。body 依舊是表示要循環執行的語句。

interface forinstatement <: statement { 

    type: "forinstatement"; 

    left: variabledeclaration |  pattern; 

    right: expression; 

declarations

聲明語句節點,同樣也是語句,隻是一個類型的細化。下邊會介紹各種聲明語句類型。

interface declaration <: statement { } 

functiondeclaration

函數聲明,和之前提到的 function 不同的是,id 不能為 null。

interface functiondeclaration <: function, declaration { 

    type: "functiondeclaration"; 

    id: identifier; 

variabledeclaration

變量聲明,kind 屬性表示是什麼類型的聲明,因為 es6 引入了 const/let。

declarations 表示聲明的多個描述,因為我們可以這樣:let a = 1, b = 2;。

interface variabledeclaration <: declaration { 

    type: "variabledeclaration"; 

    declarations: [ variabledeclarator ]; 

    kind: "var"; 

variabledeclarator

變量聲明的描述,id 表示變量名稱節點,init 表示初始值的表達式,可以為 null。

interface variabledeclarator <: node { 

    type: "variabledeclarator"; 

    id: pattern; 

    init: expression | null; 

expressions

表達式節點。

interface expression <: node { } 

thisexpression

表示 this。

interface thisexpression <: expression { 

    type: "thisexpression"; 

arrayexpression

數組表達式節點,elements 屬性是一個數組,表示數組的多個元素,每一個元素都是一個表達式節點。

interface arrayexpression <: expression { 

    type: "arrayexpression"; 

    elements: [ expression | null ]; 

objectexpression

對象表達式節點,property 屬性是一個數組,表示對象的每一個鍵值對,每一個元素都是一個屬性節點。

interface objectexpression <: expression { 

    type: "objectexpression"; 

    properties: [ property ]; 

property

對象表達式中的屬性節點。key 表示鍵,value 表示值,由于 es5 文法中有 get/set 的存在,是以有一個 kind 屬性,用來表示是普通的初始化,或者是 get/set。

interface property <: node { 

    type: "property"; 

    key: literal | identifier; 

    value: expression; 

    kind: "init" | "get" | "set"; 

functionexpression

函數表達式節點。

interface functionexpression <: function, expression { 

    type: "functionexpression"; 

下邊是一進制運算符相關的表達式部分:

unaryexpression

一進制運算表達式節點(++/-- 是 update 運算符,不在這個範疇内),operator 表示運算符,prefix 表示是否為字首運算符。argument 是要執行運算的表達式。

interface unaryexpression <: expression { 

    type: "unaryexpression"; 

    operator: unaryoperator; 

    prefix: boolean; 

unaryoperator

一進制運算符,枚舉類型,所有值如下:

enum unaryoperator { 

    "-" | "+" | "!" | "~" | "typeof" | "void" | "delete" 

updateexpression

update 運算表達式節點,即 ++/--,和一進制運算符類似,隻是 operator 指向的節點對象類型不同,這裡是 update 運算符。

interface updateexpression <: expression { 

    type: "updateexpression"; 

    operator: updateoperator; 

updateoperator

update 運算符,值為 ++ 或 --,配合 update 表達式節點的 prefix 屬性來表示前後。

enum updateoperator { 

  "++" | "--" 

下邊是二進制運算符相關的表達式部分:

binaryexpression

二進制運算表達式節點,left 和 right 表示運算符左右的兩個表達式,operator 表示一個二進制運算符。

interface binaryexpression <: expression { 

    type: "binaryexpression"; 

    operator: binaryoperator; 

    left: expression; 

binaryoperator

二進制運算符,所有值如下:

enum binaryoperator { 

    "==" | "!=" | "===" | "!==" 

         | "<" | "<=" | ">" | ">=" 

         | "<<" | ">>" | ">>>" 

         | "+" | "-" | "*" | "/" | "%" 

         | "|" | "^" | "&" | "in" 

         | "instanceof" 

assignmentexpression

指派表達式節點,operator 屬性表示一個指派運算符,left 和 right 是指派運算符左右的表達式。

interface assignmentexpression <: expression { 

    type: "assignmentexpression"; 

    operator: assignmentoperator; 

    left: pattern | expression; 

assignmentoperator

指派運算符,所有值如下:(常用的并不多)

enum assignmentoperator { 

    "=" | "+=" | "-=" | "*=" | "/=" | "%=" 

        | "<<=" | ">>=" | ">>>=" 

        | "|=" | "^=" | "&=" 

logicalexpression

邏輯運算表達式節點,和指派或者二進制運算類型,隻不過 operator 是邏輯運算符類型。

interface logicalexpression <: expression { 

    type: "logicalexpression"; 

    operator: logicaloperator; 

logicaloperator

邏輯運算符,兩種值,即與或。

enum logicaloperator { 

    "||" | "&&" 

memberexpression

成員表達式節點,即表示引用對象成員的語句,object 是引用對象的表達式節點,property 是表示屬性名稱,computed

如果為false,是表示 . 來引用成員,property 應該為一個 identifier 節點,如果 computed 屬性為

true,則是 [] 來進行引用,即property 是一個 expression 節點,名稱是表達式的結果值。

interface memberexpression <: expression, pattern { 

    type: "memberexpression"; 

    property: expression; 

    computed: boolean; 

下邊是其他的一些表達式:

conditionalexpression

條件表達式,通常我們稱之為三元運算表達式,即 boolean ? true : false。屬性參考條件語句。

interface conditionalexpression <: expression { 

    type: "conditionalexpression"; 

    alternate: expression; 

    consequent: expression; 

callexpression

函數調用表達式,即表示了 func(1, 2) 這一類型的語句。callee 屬性是一個表達式節點,表示函數,arguments 是一個數組,元素是表達式節點,表示函數參數清單。

interface callexpression <: expression { 

    type: "callexpression"; 

    callee: expression; 

    arguments: [ expression ]; 

newexpression

new 表達式。

interface newexpression <: callexpression { 

    type: "newexpression"; 

sequenceexpression

這個就是逗号運算符建構的表達式(不知道确切的名稱),expressions 屬性為一個數組,即表示構成整個表達式,被逗号分割的多個表達式。

interface sequenceexpression <: expression { 

    type: "sequenceexpression"; 

    expressions: [ expression ]; 

patterns

模式,主要在 es6 的解構指派中有意義,在 es5 中,可以了解為和 identifier 差不多的東西。

interface pattern <: node { } 

這一部分的内容比較多,但都可以舉一反三,寫這個的時候我就當把 javascript 文法再複習一遍。這個文檔還有

es2015,es2016,es2017

相關的内容,涉及的東西也蠻多,但是了解了上邊的這一些,然後從文法層面去思考這個文檔,其他的内容也就很好了解了,這裡略去,有需要請參閱:the

estree spec。

plugins

回到我們的主角,acorn,提供了一種擴充的方式來編寫相關的插件:acorn plugins。

我們可以使用插件來擴充解析器,來解析更多的一些文法,如 .jsx 文法,有興趣的看看這個插件:acorn-jsx。

官方表示 acorn 的插件是用于友善擴充解析器,但是需要對 acorn 内部的運作極緻比較了解,擴充的方式會在原本的基礎上重新定義一些方法。這裡不展開講了,如果我需要插件的話,會再寫文章聊聊這個東西。

examples

現在我們來看一下如何應用這個解析器,例如我們需要用來解析出一個符合 commonjs 規範的子產品依賴了哪些子產品,我們可以用 acorn 來解析 require 這個函數的調用,然後取出調用時的傳入參數,便可以擷取依賴的子產品。

下邊是示例代碼:

// 周遊所有節點的函數 

function walknode(node, callback) { 

  callback(node) 

  // 有 type 字段的我們認為是一個節點 

  object.keys(node).foreach((key) => { 

    const item = node[key] 

    if (array.isarray(item)) { 

      item.foreach((sub) => { 

        sub.type && walknode(sub, callback) 

      }) 

    item && item.type && walknode(item, callback) 

  }) 

function parsedependencies(str) { 

  const ast = acorn.parse(str, { ranges: true }) 

  const resource = [] // 依賴清單 

  // 從根節點開始 

  walknode(ast, (node) => { 

    const callee = node.callee 

    const args = node.arguments 

    // require 我們認為是一個函數調用,并且函數名為 require,參數隻有一個,且必須是字面量 

    if ( 

      node.type === 'callexpression' && 

      callee.type === 'identifier' && 

      callee.name === 'require' && 

      args.length === 1 && 

      args[0].type === 'literal' 

    ) { 

      const args = node.arguments 

      // 擷取依賴的相關資訊 

      resource.push({ 

        string: str.substring(node.range[0], node.range[1]), 

        path: args[0].value, 

        start: node.range[0], 

        end: node.range[1] 

  return resource 

這隻是簡單的一個情況的處理,但是已經給我們呈現了如何使用解析器,webpack 則在這個的基礎上做了更多的東西,包括 var r = require; r('a') 或者 require.async('a') 等的處理。

ast 這個東西對于前端來說,我們無時無刻不在享受着它帶來的成果(子產品建構,代碼壓縮,代碼混淆),是以了解一下總歸有好處。

作者:teabyii

來源:51cto

繼續閱讀