Douglas Crockford 是 JSON 的發明者,所以通過 DC 的程式碼來學習 JSON 和 parser 絕對是上乘之選。這個倉庫裡面有四個 JS 檔案,今天我們先研究 json_parse.js。
json_parse 定義瞭如下 API:
json_parse(string) => object
json_parse(string, (key,value)=>newValue ) => object
複製程式碼
今天我們只研究第一種 API。
程式碼結構
用 WebStorm 開啟原始碼方便閱讀,把主要函式摺疊起來,就會發現程式碼結構非常清晰,完整結構如下:
var json_parse = (function(){
`use strict`
var at; // The index of the current character
var ch; // The current character
var escape = {...}
var text
var error = function(){...}
var next = function(){...}
var number = function(){...}
var string = function(){...}
var white = function(){...}
var word = function(){...}
var array = function(){...}
var object = function(){...}
var value = function(){...}
return function parser(source, reciver){...}
}())
複製程式碼
程式碼首先用一個立即執行函式造出一個區域性作用域,ES 6 中我們只需要用 block 和 let 代替就行了。
思路
主要思路在最後一個 parser 函式裡,我們來看一下:
return function (source, reviver) {
var result;
text = source;
at = 0;
ch = " ";
result = value();
white();
if (ch) {
error("Syntax error");
}
return result;
};
複製程式碼
看起來毫無邏輯呀。
為什麼我老是說「看原始碼的投入產出比很低」呢,因為你需要看完所有程式碼,才知道主要邏輯是在做什麼。
還好程式碼不多,我看完之後總結作者的思路如下。
有三個重要的變數,ch、at 和 text
- ch 指向一個字元(實際上是複製了字元的值,但是用指向更好理解原始碼),ch 預設指向一個空字串(不要問這個空字串有什麼意義,主要是為了讓程式碼簡潔)
- at 指向下一個字元,at 儲存了下一個字元的索引(index)
- text 包含了所有字元,也就是一個符合 JSON 語法的字串
接下來我們定義一個動作:吃。
- 吃,表示將 ch 指向 at 所指的字元,然後 at 指向下一個字元。
- 吃一個空格,表示 ch 指向的字元必須是一個空格,然後吃(吃的定義見第一條);換句話說,吃一個空格的意思就是:我吃掉的字元必須是空格,不是空格就報錯。
- 吃一個{,表示我吃掉的字元必須是{,否則就報錯
- 吃一個},表示我吃掉的字元必須是},否則就報錯
- 以此類推……
好了,parser 的難點講完了,接下來就是細節了,假設 text 是字串 { “name” : “Frank” },一次完整的邏輯如下
ch=" ",at=0, text=`{ "name" : "Frank" }`
- 吃一個空格。由於 ch 一開始的預設值是空格,所以這個空格就被吃掉了,然後 ch 指向text 的第一個字元,at 指向 ch 後面一個字元(存下標,也就是1)。
- 如果 ch 是空格就繼續吃,吃到 ch 不是空格為止。
- 發現 ch 是
{
,就說明這是一個物件,生成一個空物件 object 用來儲存 key 和 value。而且後面的字元就要按照物件的語法來吃。 - 吃空格直到遇到非空格。理論上
{
後面應該接一個"key"
,所以這個非空格必須是"
。 - 吃一個
"
- 吃 N 個非
"
的字元(N >= 0) - 吃一個
"
- 把剛才吃到的 N 個字元作為一個 key,放到空物件 object 裡
- 吃空格直到遇到非空格。理論上
"key"
後面應該接:
所以這個非空格必須是:
- 吃一個
:
- 吃空格直到遇到非空格。理論上冒號後面應該接 value,value 的值可以是物件、陣列、字串、bool、null 等,所以不能預期這個非空格是什麼
- 發現是一個
"
,吃掉這個"
,如果值是一個字串 - 吃 N 個非
"
的字元 - 吃一個
"
- 把剛才吃到的 N 個字元作為一個 value,放到空物件 object 裡
- 吃空格直到遇到非空格。理論上 value 後面可以接逗號或者
}
- 發現 ch 是
}
,吃掉}
,說明 object 的資料已經讀完了 - 一直吃空格,如果發現非空格,說明語法錯誤,報錯。
- 將 object 返回,這個 object 就是 text 對應的資料了。
如果你能在大腦裡過一遍這個過程,就可以看懂所有原始碼了:
var json_parse = (function(){
`use strict`
var at; // The index of the current character
var ch; // The current character
var escape = {...}
var text
var error = function(){...}
var next = 吃(){}
var number = 吃一個完整的數字(){...}
var string = 吃一個完整的字串(){...}
var white = 吃N個空格(){...}
var word = 吃true/false/null這幾個單詞(){...}
var array = 吃一個完整的字串(){...}
var object = 吃一個物件(){...}
var value = 吃一個值,包括物件陣列字串陣列bool和null(){...}
return function parser(source, reciver){...}
}())
複製程式碼
然後我們就可以重點看主邏輯了:
return function (source, reviver) {
var result;
text = source;
at = 0;
ch = " ";
result = value(); // 吃一個值
white(); // 吃掉後面的空格
if (ch) { // 如果空格後面還有字元,就是語法錯誤了
error("Syntax error");
}
return result;
};
複製程式碼
也就是說主邏輯其實很簡單
- 用 value() 吃一個值,這個值就是 text 對應的資料
- 繼續吃掉所有空格
- 吃完發現還有字元(一定是非空格),就說明語法錯了(畫蛇添足)
接下來我們看 value() 的邏輯
value = function () {
white();
switch (ch) {
case "{":
return object();
case "[":
return array();
case """:
return string();
case "-":
return number();
default:
return (ch >= "0" && ch <= "9")
? number()
: word();
}
};
複製程式碼
邏輯也很簡單:
- 吃掉所有空格。
- 看當前的字元(ch)是什麼
- 如果 ch 是
{
,就吃一整個物件,然後把物件返回 - 如果 ch 是
[
,就吃一整個陣列,然後把陣列返回 - 如果 ch 是
"
,就吃一整個字串,然後把字串返回 - 如果 ch 是
-
,就吃一整個數字,然後把數字返回 - 如果 ch 是
0
~9
,就吃一整個數字,然後把數字返回 - 其他情況只可能是
true
/false
/null
,見啥吃啥,然後返回
圖示如下:
DC 用 ch >= "0" && ch <= "9"
來判斷字元是不是 0~9,這用到了 ASCII 字符集,如果你不懂就去搜一下。
大家應該對如何吃一個物件最感興趣,我們來看看 object() 的邏輯
var object = function () {
var key;
var obj = {};
if (ch === "{") { // 當前字元必然是 {
next("{"); // 吃掉這個 {
white(); // 吃掉所有空格
if (ch === "}") { // 遇到 } 說明物件結束了
next("}"); // 吃掉這個 }
return obj; // 返回空物件
}
while (ch) { // 沒有遇到 } 說明有 key
key = string(); // 吃一個 string 當做 key
white(); // 吃掉所有空格
next(":"); // 吃掉一個 :
if (Object.hasOwnProperty.call(obj, key)) {
error("Duplicate key `" + key + "`");
} // 如果這個 key 之前遇到過就報錯
obj[key] = value();// 把key當做object的key,然後吃一個value作為值
white(); // 吃掉所有空格
if (ch === "}") { // 如果遇到 } 說明物件結束了
next("}"); // 吃掉這個 }
return obj; // 返回物件
}
next(","); // 沒有遇到 } 說明還有 key,吃一個逗號
white(); // 吃掉空格然後繼續回到上面吃 key
}
}
error("Bad object"); // 如果執行到這裡說明語法有問題
};
複製程式碼
到此我們基本搞清楚 DC 的 json_parser 的思路了,大家可以自己看一下 white()
、array()
的原始碼,結構十分清晰。
下次我們講 json_parse_state.js 如何使用狀態機的思路重寫了這個 parser。
我的微信公眾號:搜尋 XDML 四個字母即可,XDML 是「寫程式碼啦」的拼音首字母。