经过阅读 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 是「写代码啦」的拼音首字母。

相关文章
相关标签/搜索