在 part1 當中我們提到了如何撰寫一個 JSON 解析器,並實作解析字串的功能,接下來我們把其他函數補上。(事實上只要知道基本原理,剩下的函數實作都是照本宣科而已)
Number
number 實作上也不難,容易忽略的地方在於小數點、負數、浮點數的部分,還有指數表達(1e6)。(話說剛剛才發現 E
也可以)
function number(parser) {
let str = "";
if (parser.current() === "-") {
str += "-";
parser.index += 1;
}
let curr = "";
while (((curr = parser.current()), curr >= "0" && curr <= "9")) {
str += curr;
parser.index += 1;
}
let isFloat = false;
// float number
if (parser.next(".")) {
str += ".";
isFloat = true;
while (((curr = parser.current()), curr >= "0" && curr <= "9")) {
str += curr;
parser.index += 1;
}
}
// exponential expression
let expo = "";
if (parser.next("e")) {
curr = "";
if (parser.next("-")) {
expo += "-";
}
while (((curr = parser.current()), curr >= "0" && curr <= "9")) {
expo += curr;
parser.index += 1;
}
}
if (expo) {
return isFloat
? parseFloat(str, 10) * Math.pow(10, +expo)
: parseInt(str, 10) * Math.pow(10, +expo);
}
return isFloat ? parseFloat(str, 10) : parseInt(str, 10);
}
-
第一個部分,我們先看看它是不是負數
-
第二部分,跑 while 迴圈把數字部分放入
-
第三部分,判斷是否有小數點
- 如果有的話就再爬一次數字
-
第四部分,判斷是否有指數表達(大寫或小寫 e)
- 如果有的話也再爬一次數字
-
第五部分:把字串變成數字(使用 parseInt 或是 parseFloat)
關鍵字(true, false, null)
function keyword(parser) {
if (parser.next("true")) {
return true;
} else if (parser.next("false")) {
return false;
} else if (parser.next("null")) {
return null;
}
}
這部分很簡單,就看看是不是值有匹配。
陣列(Array)
function array(parser) {
const arr = [];
if (parser.current() === "[") {
parser.next("[");
parser.skip();
if (parser.next("]")) {
return arr;
}
let i = 0;
while (parser.current()) {
const val = value(parser);
arr.push(val);
parser.skip();
if (parser.current() === "]") {
parser.next("]");
return arr;
}
parser.next(",");
parser.skip();
}
}
return arr;
}
-
第一個部分,先看看是否是
[
開頭- 如果遇到
]
代表他是一個空陣列
- 如果遇到
-
第二部分,跑 while 迴圈執行
value
函數,並放到 array 當中 -
遇到
]
代表陣列結尾,回傳 array。 -
遇到逗號代表還有下一個元素,繼續執行
這樣一來就算是差不多了,可以看一下 Repository 上的程式碼實作,可以發現測試當中有一個 specical-character
是失敗的,因為字串中可能會有一些跳脫字元,我們試著實作看看。
const escape = {
'"': '"',
t: "\t",
r: "\r",
"\\": "\\",
};
while (((curr = parser.current()), curr)) {
if (parser.next('"')) {
return str;
} else if (curr === "\\") {
parser.index += 1;
const escapeCh = parser.current();
if (escape[escapeCh]) {
str += escape[escapeCh];
}
} else {
str += curr;
}
parser.index += 1;
}
我們建立一個跳脫字元表,查表後取代成對應的實作,這裡只有實作 \t
跟 \r
。這樣子就算是通過基本的 JSON 測試了🍻。不過跳脫字元除了以上提到的之外,還要實作 \u
,代表 unicode,這個功能蠻重要的。
客製功能:
既然解析部分是自己寫,我們當然可以自己加入新的語法!假設今天要實作一個模板功能,任何用 \$\$
包起來的變數,都會用傳入的物件取代,例如:
{
"name": $name$
}
會變成:
new Parser(string, { name: 'kalan' }).parse();
// { name: "kalan" }
實作
function template(parser) {
parser.skip();
if (parser.next("$")) {
parser.skip();
if (parser.next("$")) {
throw new Error("template can not be empty");
}
let curr = "";
let key = "";
while (((curr = parser.current()), curr)) {
if (parser.next("$")) {
return parser.variables[key];
}
key += curr;
parser.index += 1;
}
}
}
- 首先先匹配
$
- 開始讀取內容,直到看到
$
- 遇到
$
停止while
迴圈並用變數將模板變數取代,回傳結果
詳細的實作可以到 template 這個 branch 上查看,可以瞧瞧測試結果(在 test/template
資料夾當中)
結尾
透過自己撰寫解析器,我們可以將比較複雜的實作用比較容易表達語法表現。甚至也可以在既有的文法中(如這次的 JSON)做一層擴充,加入自己想要的功能。雖然不是那麼實用,但目的是為了展示透過解析能夠做到的事情有哪些。
雖然解析本身很重要也很有趣,但解析語言本身只能算是第一步而已,就像如果只是把 JSX 純粹轉成 JavaScript 的程式碼,沒有 React 的幫助也沒有用;把 SQL 變成抽象語法樹但沒有資料庫的實作,也是有點雞肋。解析語言的目的是為了方便後續的處理(執行 Query、渲染到 DOM)。
事實上現在也已經有很多的函式庫,幫助你直接省略解析器這一塊,像是著名的 Bison 或是 PEG.js,都是讓你用類似 BNF 的語法自動幫你產生一個穩定的解析器,省下自己解析的時間,直接專注在語言實作上。
我們這次的 JSON 解析器並沒有做轉換成抽象語法樹再來產生最後結果,所以我們下一個階段,會試著解析簡單的 HTML,並且轉換成語法樹後用 JavaScript 的 DOM API 渲染出來。