本文的目的,不是針對現有的可用於生產環境的JSON、XML解析器原始碼進行剖析,而是介紹文字掃描的基礎方法
next(char)
,並以此為核心武器,根據目標語言的詞法和語法特點,一步步地組織出條例清晰、易維護的解析器程式碼。希望這會是一篇實踐性強,讓您有所收穫的文章。另外,這裡需要提前說明的是,本文所實現的解析器僅作為coding練習使用。一些目標語言的規範中提到的語法,可能無法正常解析。另外,本文所實現的解析器也缺少大量的例項進行測試。請不要用於生產用途。
前言
作為一個非科班前端程式設計師,我最近特別痴迷於自學《編譯原理》這門課。原因在於,自己大學時代的專業是語言學,其中的理論有頗多相似之處;再加上前端工作中,模版編譯成render function,webpack通過loader載入檔案等都方面涉及到了編譯。我也希望自己能多瞭解一些編譯知識,說不定能在日後的前端工作中能夠發揮奇效。
看了一些youtube上的公開課資源,啃了一點龍書這樣的編譯原理經典作品後,我感覺自己只瞭解了一堆關於詞法法解析、語法解析的理論總結,很難從中獲得“學會了”、“會用了”這樣的成就感。於是在稍稍有了一點知識基礎後,我開始尋找github上關於解析器的原始碼。
JSON的解析
這裡想給大家推薦的是JSON之父,Douglas Crockford的repo: JSON-js中的這個原始碼,它也是本文的靈感源泉:
JSON-js/json_parse.js at master · douglascrockford/JSON-js
據程式碼註釋,這個檔案實現了JSON.parse方法,使用的解析手段是recursive decending(遞迴下降分析)。
在同一個repo裡還有一個
json_parse_state.js
檔案,也是JSON.parse方法的實現,使用的解析手段是state machine(狀態機)。其實我個人認為上文連結中的原始碼使用的解析手段也是state machine,因為recursive decending應該是語法分析使用的方法來著= =。
但從程式碼的清晰度上來看,json_parse.js要好不少,所以更推薦閱讀。
快速地過一遍原始碼,我們可以發現一個核心函式:
var next = function (c) {
// If a c parameter is provided, verify that it matches the current character.
if (c && c !== ch) {
error("Expected '" + c + "' instead of '" + ch + "'");
}
// Get the next character. When there are no more characters,
// return the empty string.
ch = text.charAt(at);
at += 1;
return ch;
};
複製程式碼
這個方法相當於一個字元掃描器,其中使用的全域性變數at
是當前掃描游標所處位置的索引,ch
是當前掃描游標所處位置的字元。呼叫next方法時,如果傳入了引數c
(也是一個字元),則會比較此字元與當前掃描器所在的字元,如果不相同就會報錯,並且掃描游標不會向前移動;如果未傳引數,掃描游標的位置和所指的字元都會向前更新一個位置。
這份程式碼中的其他函式,充斥著對next的呼叫,讓我們來看幾個例子感受一下next的用法。
var word = function () {
// true, false, or null.
switch (ch) {
case "t":
next("t");
next("r");
next("u");
next("e");
return true;
case "f":
next("f");
next("a");
next("l");
next("s");
next("e");
return false;
case "n":
next("n");
next("u");
next("l");
next("l");
return null;
}
error("Unexpected '" + ch + "'");
};
複製程式碼
word函式用來處理JSON中的三個常量token,即true
, false
和null
。整個函式根據首字母,分別接收t->r->u->e,f->a->l->s->e,n->u->l->l這樣的字元輸入。如果其中出現了其他順序的字元輸入,都會丟擲Error。word方法還會在匹配token的同時,返回所匹配到的token的值。3個return語句所出現的位置,表示word函式已經接受了這段字元輸入,併成功解析出了一個值。
再來看另一個不傳參呼叫next()的例子:
var white = function () {
// Skip whitespace.
while (ch && ch <= " ") {
next();
}
};
複製程式碼
white函式的作用僅在於跳過空白,只要當前字元是屬於空白的,就不停地呼叫next()作無條件後跳。
原始碼中還有number和string函式,其用途和上面的word, white一樣,只不過邏輯更為複雜,可以解析出不定長度、不定字元組合的數字和字串。
一步一步地寫出這些“零件”的解析函式後,我們就可以進一步寫出一些複合結構的解析函式了,也就是原始碼中的array和object函式。
最後,原始碼中實現了可以解析任意一個JSON元素的value函式。從語法的角度講,這裡所定義的value,可以是任何一個string, number, array或object,至此,我們就完成了解析所有JSON元素需要的函式。
以上就是解析的核心程式碼了,個人認為十分地易於理解並且有明確的分層,易於維護以及以後增加功能。我也在這裡用同樣的next函式的手法,嘗試重寫了這個JSON解析器原始碼。作為練習,我沒有實現escape或revive等功能,但把各個解析函式拆分得更加精細(例如為每個單字元token都寫了解析函式,將array拆解為[ + elements + ]
等),使得程式碼更易讀。地址是:
XML的解析
有了上面的JSON解析器實現的“手感”,我又嘗試著用同樣的next函式手法,部分地實現了XML的解析。和JSON相比,個人在實現過程中發現的坑點主要在於:
- JSON物件基本上就是JavaScript中Object物件的字面化表示,所以每次解析出來一小段之後,直接以JavaScript數列或物件的形式儲存即可。XML節點需要為其定義類似下面的資料結構,所以程式碼的複雜度略有增加:
Node {
tagName //節點標籤名
attrs //節點上的屬性,為key/value的陣列
children //節點的子節點,為Node的陣列
}
複製程式碼
- XML物件必須作語法分析,也就是close tag有沒有匹配的問題。諸如
<a><b></a></b>
這樣的XML需要提示解析錯誤。不過實現這個也很簡單,使用一個nodeStack棧,在opentag時推入節點;在closetag時檢查當前節點是否和棧尾的tag相匹配,匹配則推出末尾的節點;在comment節點或text節點時不作處理即可。 - comment節點的結束判斷。comment節點的格式是
<!--content-->
,因此在解析content部分時,每輸入一個字元,需要作3個字元的提前判斷。即,如果當前所讀到的字元的接下來三個字元分別是-->
時,停止解析。
我所實現的XML解析器的程式碼如下(沒有實現self-closing tag的解析功能,例如<br>
, <input>
等。所有tag必須成對出現):