經過閱讀 Douglas Crockford 的源碼學習如何寫 JSON parser(一)

JSON-jsgit

Douglas Crockford 是 JSON 的發明者,因此經過 DC 的代碼來學習 JSON 和 parser 絕對是上乘之選。這個倉庫裏面有四個 JS 文件,今天咱們先研究 json_parse.js。github

json_parse 定義了以下 API:json

json_parse(string) => object
json_parse(string, (key,value)=>newValue ) => object
複製代碼

今天咱們只研究第一種 API。數組

代碼結構

用 WebStorm 打開源碼方便閱讀,把主要函數摺疊起來,就會發現代碼結構很是清晰,完整結構以下:bash

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;
};
複製代碼

看起來毫無邏輯呀。學習

爲何我總是說「看源碼的投入產出比很低」呢,由於你須要看完全部代碼,才知道主要邏輯是在作什麼。ui

還好代碼很少,我看完以後總結做者的思路以下。spa

有三個重要的變量,ch、at 和 text

  • ch 指向一個字符(其實是複製了字符的值,可是用指向更好理解源碼),ch 默認指向一個空字符串(不要問這個空字符串有什麼意義,主要是爲了讓代碼簡潔)
  • at 指向下一個字符,at 存儲了下一個字符的索引(index)
  • text 包含了全部字符,也就是一個符合 JSON 語法的字符串

接下來咱們定義一個動做:吃。

  • 吃,表示將 ch 指向 at 所指的字符,而後 at 指向下一個字符。
  • 吃一個空格,表示 ch 指向的字符必須是一個空格,而後吃(吃的定義見第一條);換句話說,吃一個空格的意思就是:我吃掉的字符必須是空格,不是空格就報錯。
  • 吃一個{,表示我吃掉的字符必須是{,不然就報錯
  • 吃一個},表示我吃掉的字符必須是},不然就報錯
  • 以此類推……

好了,parser 的難點講完了,接下來就是細節了,假設 text 是字符串 { "name" : "Frank" },一次完整的邏輯以下

  1. ch=" ",at=0, text='{ "name" : "Frank" }'
  2. 吃一個空格。因爲 ch 一開始的默認值是空格,因此這個空格就被吃掉了,而後 ch 指向text 的第一個字符,at 指向 ch 後面一個字符(存下標,也就是1)。
  3. 若是 ch 是空格就繼續吃,吃到 ch 不是空格爲止。
  4. 發現 ch 是 {,就說明這是一個對象,生成一個空對象 object 用來存儲 key 和 value。並且後面的字符就要按照對象的語法來吃。
  5. 吃空格直到遇到非空格。理論上 { 後面應該接一個 "key",因此這個非空格必須是 "
  6. 吃一個 "
  7. 吃 N 個非 " 的字符(N >= 0)
  8. 吃一個 "
  9. 把剛纔吃到的 N 個字符做爲一個 key,放到空對象 object 裏
  10. 吃空格直到遇到非空格。理論上 "key" 後面應該接 : 因此這個非空格必須是 :
  11. 吃一個 :
  12. 吃空格直到遇到非空格。理論上冒號後面應該接 value,value 的值能夠是對象、數組、字符串、bool、null 等,因此不能預期這個非空格是什麼
  13. 發現是一個 ",吃掉這個 ",若是值是一個字符串
  14. 吃 N 個非 " 的字符
  15. 吃一個 "
  16. 把剛纔吃到的 N 個字符做爲一個 value,放到空對象 object 裏
  17. 吃空格直到遇到非空格。理論上 value 後面能夠接逗號或者 }
  18. 發現 ch 是 },吃掉 },說明 object 的數據已經讀完了
  19. 一直吃空格,若是發現非空格,說明語法錯誤,報錯。
  20. 將 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;
};
複製代碼

也就是說主邏輯其實很簡單

  1. 用 value() 吃一個值,這個值就是 text 對應的數據
  2. 繼續吃掉全部空格
  3. 吃完發現還有字符(必定是非空格),就說明語法錯了(多此一舉)

接下來咱們看 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();
    }
};
複製代碼

邏輯也很簡單:

  1. 吃掉全部空格。
  2. 看當前的字符(ch)是什麼
  3. 若是 ch 是 {,就吃一整個對象,而後把對象返回
  4. 若是 ch 是 [,就吃一整個數組,而後把數組返回
  5. 若是 ch 是 ",就吃一整個字符串,而後把字符串返回
  6. 若是 ch 是 -,就吃一整個數字,而後把數字返回
  7. 若是 ch 是 0~9,就吃一整個數字,而後把數字返回
  8. 其餘狀況只多是 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 是「寫代碼啦」的拼音首字母。

相關文章
相關標籤/搜索