本文的目的,不是針對現有的可用於生產環境的JSON、XML解析器源碼進行剖析,而是介紹文本掃描的基礎方法
next(char)
,並以此爲核心武器,根據目標語言的詞法和語法特色,一步步地組織出條例清晰、易維護的解析器代碼。但願這會是一篇實踐性強,讓您有所收穫的文章。javascript另外,這裏須要提早說明的是,本文所實現的解析器僅做爲coding練習使用。一些目標語言的規範中提到的語法,可能沒法正常解析。另外,本文所實現的解析器也缺乏大量的實例進行測試。請不要用於生產用途。前端
做爲一個非科班前端程序員,我最近特別癡迷於自學《編譯原理》這門課。緣由在於,本身大學時代的專業是語言學,其中的理論有頗多類似之處;再加上前端工做中,模版編譯成render function,webpack經過loader加載文件等都方面涉及到了編譯。我也但願本身能多瞭解一些編譯知識,說不定能在往後的前端工做中可以發揮奇效。java
看了一些youtube上的公開課資源,啃了一點龍書這樣的編譯原理經典做品後,我感受本身只瞭解了一堆關於詞法法解析、語法解析的理論總結,很難從中得到「學會了」、「會用了」這樣的成就感。因而在稍稍有了一點知識基礎後,我開始尋找github上關於解析器的源碼。node
這裏想給你們推薦的是JSON之父,Douglas Crockford的repo: JSON-js中的這個源代碼,它也是本文的靈感源泉:webpack
JSON-js/json_parse.js at master · douglascrockford/JSON-jsgit
據代碼註釋,這個文件實現了JSON.parse方法,使用的解析手段是recursive decending(遞歸降低分析)。程序員
在同一個repo裏還有一個
json_parse_state.js
文件,也是JSON.parse方法的實現,使用的解析手段是state machine(狀態機)。github其實我我的認爲上文連接中的源代碼使用的解析手段也是state machine,由於recursive decending應該是語法分析使用的方法來着= =。web
但從代碼的清晰度上來看,json_parse.js要好很多,因此更推薦閱讀。json
快速地過一遍源碼,咱們能夠發現一個核心函數:
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 + ]
等),使得代碼更易讀。地址是:
有了上面的JSON解析器實現的「手感」,我又嘗試着用一樣的next函數手法,部分地實現了XML的解析。和JSON相比,我的在實現過程當中發現的坑點主要在於:
Node {
tagName //節點標籤名
attrs //節點上的屬性,爲key/value的數組
children //節點的子節點,爲Node的數組
}
複製代碼
<a><b></a></b>
這樣的XML須要提示解析錯誤。不過實現這個也很簡單,使用一個nodeStack棧,在opentag時推入節點;在closetag時檢查當前節點是否和棧尾的tag相匹配,匹配則推出末尾的節點;在comment節點或text節點時不做處理便可。<!--content-->
,所以在解析content部分時,每輸入一個字符,須要做3個字符的提早判斷。即,若是當前所讀到的字符的接下來三個字符分別是-->
時,中止解析。我所實現的XML解析器的代碼以下(沒有實現self-closing tag的解析功能,例如<br>
, <input>
等。全部tag必須成對出現):