由於最近工做上有須要使用解析 JavaScript 的代碼,大部分狀況使用正則表達式匹配就能夠處理,可是一旦依賴於代碼上下文的內容時,正則或者簡單的字符解析就很力不從心了,這個時候須要一個語言解析器來獲取整一個 AST(abstract syntax tree)。html
而後我找到了多個使用 JavaScript 編寫的 JavaScript 解析器:前端
Esprimanode
Acornjquery
UglifyJS 2git
Shiftgithub
從提交記錄來看,維護狀況都蠻好的,ES 各類發展的特性都跟得上,我分別都簡單瞭解了一下,聊聊他們的一些狀況。正則表達式
Esprima 是很經典的一個解析器,Acorn 在它以後誕生,都是幾年前的事情了。按照 Acorn 做者的說法,當時造這個輪子更多隻是好玩,速度能夠和 Esprima 媲美,可是實現代碼更少。其中比較關鍵的點是這兩個解析器出來的 AST 結果(對,只是 AST,tokens 不同)都是符合 The Estree Spec 規範(這是 Mozilla 的工程師給出的 SpiderMonkey 引擎輸出的 JavaScript AST 的規範文檔,也能夠參考:SpiderMonkey in MDN)的,也就是獲得的結果在很大部分上是兼容的。chrome
如今很出名的 Webpack 解析代碼時用的也是 Acorn。express
至於 Uglify,很出名的一個 JavaScript 代碼壓縮器,其實它自帶了一個代碼解析器,也能夠輸出 AST,可是它的功能更多仍是用於壓縮代碼,若是拿來解析代碼感受不夠純粹。數組
Shift 這個沒作多少了解,只知道他定義了本身的一套 AST 規範。
Esprima 官網上有一個性能測試,我在 chrome 上跑的結果以下:
<img src="http://ww1.sinaimg.cn/large/0... alt="性能測試" style="width:100%;">
可見,Acorn 的性能很不錯,並且還有一個 Estree 的規範呢(規範很重要,我我的以爲遵循通用的規範是代碼複用的重要基礎),因此我就直接選用 Acorn 來作代碼解析了。
圖中作性能對比的還有 Google 的 Traceur,它更可能是一個 ES6 to ES5 的 compiler,於咱們想要找的解析器定位不符。
下面進入正題,如何使用 Acorn 來解析 JavaScript。
解析器的 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
。
詞法結果 token 和 Esprima 的結果數據結構上有必定的區別(Espree 又是作了這一層的兼容),有興趣瞭解的能夠看下 Esprima 的解析結果:http://esprima.org/demo/parse... 。
至於 Acorn 解析的 AST 和 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。
這一部分是重頭戲,由於實際上我須要的仍是解析出來的 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 的基礎語法整理了一遍。
標識符,我以爲應該是這麼叫的,就是咱們寫 JS 時自定義的名稱,如變量名,函數名,屬性名,都歸爲標識符。相應的接口是這樣的:
interface Identifier <: Expression, Pattern { type: "Identifier"; name: string; }
一個標識符多是一個表達式,或者是解構的模式(ES6 中的解構語法)。咱們等會會看到 Expression
和 Pattern
相關的內容的。
字面量,這裏不是指 []
或者 {}
這些,而是自己語義就表明了一個值的字面量,如 1
,「hello」
, true
這些,還有正則表達式(有一個擴展的 Node
來表示正則表達式),如 /d?/
。咱們看一下文檔的定義:
interface Literal <: Expression { type: "Literal"; value: string | boolean | null | number | RegExp; }
value
這裏即對應了字面量的值,咱們能夠看出字面量值的類型,字符串,布爾,數值,null
和正則。
這個針對正則字面量的,爲了更好地來解析正則表達式的內容,添加多一個 regex
字段,裏邊會包括正則自己,以及正則的 flags
。
interface RegExpLiteral <: Literal { regex: { pattern: string; flags: string; }; }
通常這個是做爲跟節點的,即表明了一棵完整的程序代碼樹。
interface Program <: Node { type: "Program"; body: [ Statement ]; }
body
屬性是一個數組,包含了多個 Statement
(即語句)節點。
函數聲明或者函數表達式節點。
interface Function <: Node { id: Identifier | null; params: [ Pattern ]; body: BlockStatement; }
id
是函數名,params
屬性是一個數組,表示函數的參數。body
是一個塊語句。
有一個值得留意的點是,你在測試過程當中,是不會找到 type: "Function"
的節點的,可是你能夠找到 type: "FunctionDeclaration"
和 type: "FunctionExpression"
,由於函數要麼以聲明語句出現,要麼以函數表達式出現,都是節點類型的組合類型,後邊會再說起 FunctionDeclaration
和 FunctionExpression
的相關內容。
這讓人感受這個文檔規劃得蠻細緻的,函數名,參數和函數塊是屬於函數部分的內容,而聲明或者表達式則有它本身須要的東西。
語句節點沒什麼特別的,它只是一個節點,一種區分,可是語句有不少種,下邊會詳述。
interface Statement <: Node { }
表達式語句節點,a = a + 1
或者 a++
裏邊會有一個 expression
屬性指向一個表達式節點對象(後邊會說起表達式)。
interface ExpressionStatement <: Statement { type: "ExpressionStatement"; expression: Expression; }
塊語句節點,舉個例子:if (...) { // 這裏是塊語句的內容 }
,塊裏邊能夠包含多個其餘的語句,因此有一個 body
屬性,是一個數組,表示了塊裏邊的多個語句。
interface BlockStatement <: Statement { type: "BlockStatement"; body: [ Statement ]; }
一個空的語句節點,沒有執行任何有用的代碼,例如一個單獨的分號 ;
interface EmptyStatement <: Statement { type: "EmptyStatement"; }
debugger
,就是表示這個,沒有其餘了。
interface DebuggerStatement <: Statement { type: "DebuggerStatement"; }
with
語句節點,裏邊有兩個特別的屬性,object
表示 with
要使用的那個對象(能夠是一個表達式),body
則是對應 with
後邊要執行的語句,通常會是一個塊語句。
interface WithStatement <: Statement { type: "WithStatement"; object: Expression; body: Statement; }
下邊是控制流的語句:
返回語句節點,argument
屬性是一個表達式,表明返回的內容。
interface ReturnStatement <: Statement { type: "ReturnStatement"; argument: Expression | null; }
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; body: Statement; }
break
語句節點,會有一個 label
屬性表示須要的 label
名稱,當不須要 label
的時候(一般都不須要),即是 null
。
interface BreakStatement <: Statement { type: "BreakStatement"; label: Identifier | null; }
continue
語句節點,和 break
相似。
interface ContinueStatement <: Statement { type: "ContinueStatement"; label: Identifier | null; }
下邊是條件語句:
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; }
switch
語句節點,有兩個屬性,discriminant
屬性表示 switch
語句後緊隨的表達式,一般會是一個變量,cases
屬性是一個 case
節點的數組,用來表示各個 case
語句。
interface SwitchStatement <: Statement { type: "SwitchStatement"; discriminant: Expression; cases: [ SwitchCase ]; }
switch
的 case
節點。test
屬性表明這個 case
的判斷表達式,consequent
則是這個 case
的執行語句。
當 test
屬性是 null
時,則是表示 default
這個 case
節點。
interface SwitchCase <: Node { type: "SwitchCase"; test: Expression | null; consequent: [ Statement ]; }
下邊是異常相關的語句:
throw
語句節點,argument
屬性用以表示 throw
後邊緊跟的表達式。
interface ThrowStatement <: Statement { type: "ThrowStatement"; argument: Expression; }
try
語句節點,block
屬性表示 try
的執行語句,一般是一個塊語句。
hanlder
屬性是指 catch
節點,finalizer
是指 finally
語句節點,當 hanlder
爲 null
時,finalizer
必須是一個塊語句節點。
interface TryStatement <: Statement { type: "TryStatement"; block: BlockStatement; handler: CatchClause | null; finalizer: BlockStatement | null; }
catch
節點,param
用以表示 catch
後的參數,body
則表示 catch
後的執行語句,一般是一個塊語句。
interface CatchClause <: Node { type: "CatchClause"; param: Pattern; body: BlockStatement; }
下邊是循環語句:
while
語句節點,test
表示括號中的表達式,body
是表示要循環執行的語句。
interface WhileStatement <: Statement { type: "WhileStatement"; test: Expression; body: Statement; }
do/while
語句節點,和 while
語句相似。
interface DoWhileStatement <: Statement { type: "DoWhileStatement"; body: Statement; test: Expression; }
for
循環語句節點,屬性 init/test/update
分別表示了 for
語句括號中的三個表達式,初始化值,循環判斷條件,每次循環執行的變量更新語句(init
能夠是變量聲明或者表達式)。這三個屬性均可覺得 null
,即 for(;;){}
。 body
屬性用以表示要循環執行的語句。
interface ForStatement <: Statement { type: "ForStatement"; init: VariableDeclaration | Expression | null; test: Expression | null; update: Expression | null; body: Statement; }
for/in
語句節點,left
和 right
屬性分別表示在 in
關鍵詞左右的語句(左側能夠是一個變量聲明或者表達式)。body
依舊是表示要循環執行的語句。
interface ForInStatement <: Statement { type: "ForInStatement"; left: VariableDeclaration | Pattern; right: Expression; body: Statement; }
聲明語句節點,一樣也是語句,只是一個類型的細化。下邊會介紹各類聲明語句類型。
interface Declaration <: Statement { }
函數聲明,和以前提到的 Function 不一樣的是,id
不能爲 null
。
interface FunctionDeclaration <: Function, Declaration { type: "FunctionDeclaration"; id: Identifier; }
變量聲明,kind
屬性表示是什麼類型的聲明,由於 ES6 引入了 const/let
。declarations
表示聲明的多個描述,由於咱們能夠這樣:let a = 1, b = 2;
。
interface VariableDeclaration <: Declaration { type: "VariableDeclaration"; declarations: [ VariableDeclarator ]; kind: "var"; }
變量聲明的描述,id
表示變量名稱節點,init
表示初始值的表達式,能夠爲 null
。
interface VariableDeclarator <: Node { type: "VariableDeclarator"; id: Pattern; init: Expression | null; }
表達式節點。
interface Expression <: Node { }
表示 this
。
interface ThisExpression <: Expression { type: "ThisExpression"; }
數組表達式節點,elements
屬性是一個數組,表示數組的多個元素,每個元素都是一個表達式節點。
interface ArrayExpression <: Expression { type: "ArrayExpression"; elements: [ Expression | null ]; }
對象表達式節點,property
屬性是一個數組,表示對象的每個鍵值對,每個元素都是一個屬性節點。
interface ObjectExpression <: Expression { type: "ObjectExpression"; properties: [ Property ]; }
對象表達式中的屬性節點。key
表示鍵,value
表示值,因爲 ES5 語法中有 get/set
的存在,因此有一個 kind
屬性,用來表示是普通的初始化,或者是 get/set
。
interface Property <: Node { type: "Property"; key: Literal | Identifier; value: Expression; kind: "init" | "get" | "set"; }
函數表達式節點。
interface FunctionExpression <: Function, Expression { type: "FunctionExpression"; }
下邊是一元運算符相關的表達式部分:
一元運算表達式節點(++/--
是 update 運算符,不在這個範疇內),operator
表示運算符,prefix
表示是否爲前綴運算符。argument
是要執行運算的表達式。
interface UnaryExpression <: Expression { type: "UnaryExpression"; operator: UnaryOperator; prefix: boolean; argument: Expression; }
一元運算符,枚舉類型,全部值以下:
enum UnaryOperator { "-" | "+" | "!" | "~" | "typeof" | "void" | "delete" }
update 運算表達式節點,即 ++/--
,和一元運算符相似,只是 operator
指向的節點對象類型不一樣,這裏是 update 運算符。
interface UpdateExpression <: Expression { type: "UpdateExpression"; operator: UpdateOperator; argument: Expression; prefix: boolean; }
update 運算符,值爲 ++
或 --
,配合 update 表達式節點的 prefix
屬性來表示先後。
enum UpdateOperator { "++" | "--" }
下邊是二元運算符相關的表達式部分:
二元運算表達式節點,left
和 right
表示運算符左右的兩個表達式,operator
表示一個二元運算符。
interface BinaryExpression <: Expression { type: "BinaryExpression"; operator: BinaryOperator; left: Expression; right: Expression; }
二元運算符,全部值以下:
enum BinaryOperator { "==" | "!=" | "===" | "!==" | "<" | "<=" | ">" | ">=" | "<<" | ">>" | ">>>" | "+" | "-" | "*" | "/" | "%" | "|" | "^" | "&" | "in" | "instanceof" }
賦值表達式節點,operator
屬性表示一個賦值運算符,left
和 right
是賦值運算符左右的表達式。
interface AssignmentExpression <: Expression { type: "AssignmentExpression"; operator: AssignmentOperator; left: Pattern | Expression; right: Expression; }
賦值運算符,全部值以下:(經常使用的並很少)
enum AssignmentOperator { "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "<<=" | ">>=" | ">>>=" | "|=" | "^=" | "&=" }
邏輯運算表達式節點,和賦值或者二元運算類型,只不過 operator
是邏輯運算符類型。
interface LogicalExpression <: Expression { type: "LogicalExpression"; operator: LogicalOperator; left: Expression; right: Expression; }
邏輯運算符,兩種值,即與或。
enum LogicalOperator { "||" | "&&" }
成員表達式節點,即表示引用對象成員的語句,object
是引用對象的表達式節點,property
是表示屬性名稱,computed
若是爲 false
,是表示 .
來引用成員,property
應該爲一個 Identifier
節點,若是 computed
屬性爲 true
,則是 []
來進行引用,即 property
是一個 Expression
節點,名稱是表達式的結果值。
interface MemberExpression <: Expression, Pattern { type: "MemberExpression"; object: Expression; property: Expression; computed: boolean; }
下邊是其餘的一些表達式:
條件表達式,一般咱們稱之爲三元運算表達式,即 boolean ? true : false
。屬性參考條件語句。
interface ConditionalExpression <: Expression { type: "ConditionalExpression"; test: Expression; alternate: Expression; consequent: Expression; }
函數調用表達式,即表示了 func(1, 2)
這一類型的語句。callee
屬性是一個表達式節點,表示函數,arguments
是一個數組,元素是表達式節點,表示函數參數列表。
interface CallExpression <: Expression { type: "CallExpression"; callee: Expression; arguments: [ Expression ]; }
new
表達式。
interface NewExpression <: CallExpression { type: "NewExpression"; }
這個就是逗號運算符構建的表達式(不知道確切的名稱),expressions
屬性爲一個數組,即表示構成整個表達式,被逗號分割的多個表達式。
interface SequenceExpression <: Expression { type: "SequenceExpression"; expressions: [ Expression ]; }
模式,主要在 ES6 的解構賦值中有意義,在 ES5 中,能夠理解爲和 Identifier
差很少的東西。
interface Pattern <: Node { }
這一部分的內容比較多,但均可以觸類旁通,寫這個的時候我就當把 JavaScript 語法再複習一遍。這個文檔還有 ES2015,ES2016,ES2017 相關的內容,涉及的東西也蠻多,可是理解了上邊的這一些,而後從語法層面去思考這個文檔,其餘的內容也就很好理解了,這裏略去,有須要請參閱:The Estree Spec。
回到咱們的主角,Acorn,提供了一種擴展的方式來編寫相關的插件:Acorn Plugins。
咱們可使用插件來擴展解析器,來解析更多的一些語法,如 .jsx
語法,有興趣的看看這個插件:acorn-jsx。
官方表示 Acorn 的插件是用於方便擴展解析器,可是須要對 Acorn 內部的運行極致比較瞭解,擴展的方式會在本來的基礎上從新定義一些方法。這裏不展開講了,若是我須要插件的話,會再寫文章聊聊這個東西。
如今咱們來看一下如何應用這個解析器,例如咱們須要用來解析出一個符合 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 這個東西對於前端來講,咱們無時無刻不在享受着它帶來的成果(模塊構建,代碼壓縮,代碼混淆),因此瞭解一下總歸有好處。
有問題歡迎討論。