本文由 简悦 SimpRead 转码, 原文地址 juejin.cn

    近日在翻红宝书,看到 JSON 那一章节,忽然想到:“如何用 JS 实现 JSON.parse?” 带着这个疑问,我找到了 JSON 之父 Douglas Crockford 写的 ployfill,里面提供了三种实现方式,下面我们逐一来分析。

    第一种方式最简单,也最直观,就是直接调用 eval,代码如下:

    1. var json = '{"a":"1", "b":2}';
    2. var obj = eval("(" + json + ")"); // obj 就是 json 反序列化之后得到的对象复制代码

    因为 JSON 脱胎于 JS,同时也是 JS 的子集,所以能够直接交给 eval 运行。
    然而,通常我们都说 eval 是邪恶的,尽量不要使用。为什么这里又用了呢?其实 eval 并不邪恶,只是对于新手来说,用了容易出问题,所以不建议使用而已。如果你水平够高,能正确地使用 Eval,那么它还是有很多用处的,比如 静态模板

    ok,回到上面,我们像新手一样直接调用 eval,会不会出问题呢? → 会,这里有 XSS 漏洞。触发条件:参数 json 并非真正的 JSON 数据,而是可执行的 JS 代码。
    那么,该如何规避这个问题呢?→ 老手 Douglas Crockford 给我们做了示范:对参数 json 做校验,只有真正符合 JSON 格式,才能调用 eval,具体就是下面这几个正则匹配。

    1. // We split the second stage into 4 regexp operations in order to work around
    2. // crippling inefficiencies in IE's and Safari's regexp engines. First we
    3. // replace the JSON backslash pairs with "@" (a non-JSON character). Second, we
    4. // replace all simple value tokens with "]" characters. Third, we delete all
    5. // open brackets that follow a colon or comma or that begin the text. Finally,
    6. // we look to see that the remaining characters are only whitespace or "]" or
    7. // "," or ":" or "{" or "}". If that is so, then the text is safe for eval.
    8. var rx_one = /^[\],:{}\s]*$/;
    9. var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
    10. var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
    11. var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
    12. if (
    13. rx_one.test(
    14. json
    15. .replace(rx_two, "@")
    16. .replace(rx_three, "]")
    17. .replace(rx_four, "")
    18. )
    19. ) {
    20. var obj = eval("(" +json + ")");
    21. }复制代码

    看到上面的代码,你是否对那么复杂的正则感到头晕呢?反正我是很晕,所以我找了一个非常好用的正则可视化工具 Regexper 来帮我看懂这些正则,如下图所示。
    JSON.parse 三种实现方式 - 图1
    JSON.parse 三种实现方式 - 图2
    JSON.parse 三种实现方式 - 图3

    有 2 个地方需要注意:

    1. 英文注释中提到:
      表面上看起来要删除 open brackets 开括号(,而实际上正则 rx_four 匹配删除的却是[,这是为什么呢?因为中英文语义的不同。在中文里,开括号一般指(,而在英文里开括号一般指[,其间细微差别需要知道。

      Third, we delete all open brackets that follow a colon or comma or that begin the text.

    2. 看 rx_three,里面有(?:)结构,这是正则的不捕获分组,具体可以参考这里。使用不捕获分组的原因:要解析的 json 有可能是一个很大的 JSON,如果匹配到的每个 token 都缓存起来的话,那么对内存的消耗是巨大的,而这里我们只想替换字符,并不需要知道都匹配到了哪些字符。

    拓展阅读:

    1. JavaScript 为什么不推荐使用 eval?
    2. JSON: 不要误会, 我真的不是 JavaScript 的子集,翻译 By 浪子,原作者 By Magnus Holm

    第一种 eval 的方法,相当于一股脑儿把 JSON 字符串塞进去。其实我们还可以手动逐个字符地扫描,然后进行判断,这就是第二种方法:递归。

    1. // 调用核心的 next 函数,逐个读取字符
    2. var next = function (c) {
    3. // If a c parameter is provided, verify that it matches the current character.
    4. if (c && c !== ch) {
    5. error("Expected '" + c + "' instead of '" + ch + "'");
    6. }
    7. // Get the next character. When there are no more characters,
    8. // return the empty string.
    9. ch = text.charAt(at);
    10. at += 1;
    11. return ch;
    12. };复制代码

    所谓 “递归”,就是重复调用 value 函数。

    1. value = function () {
    2. // Parse a JSON value. It could be an object, an array, a string, a number,
    3. // or a word.
    4. white();
    5. // 根据当前字符是什么,我们便能推导出后面应该接的是什么类型
    6. switch (ch) {
    7. case "{":
    8. return object();
    9. case "[":
    10. return array();
    11. case "\"":
    12. return string();
    13. case "-":
    14. return number();
    15. default:
    16. return (ch >= "0" && ch <= "9")
    17. ? number()
    18. : word();
    19. }
    20. };复制代码

    还是以 '{"a":"1", "b":2}' 为例,程序大致逻辑是:启动 → 首次调用 value() → 发现是 { → 原来是对象,走 object() → 通过 string() 得到 key 值为 “a” → 读取到冒号,哦,后面可能是对象、数组、布尔值等等,具体是什么,还得再次调用 value() 才知道 → ……

    这种实现方案,既没有用 eval,也没有用正则,单纯靠逐个读取字符,所以代码逻辑比较复杂,需要多 debug 才能理清逻辑。lqt0223 也曾分析过这种实现方式:自己实现 JSON、XML 的解析 没那么难

    状态机名字起得很抽象,应用也非常广泛,比如正则引擎、词法分析,甚至是字符串匹配的 KMP 算法都能用它来解释。它代表着一种本质的逻辑:在 A 状态下,如果输入 B,就会转移到 C 状态

    那么,状态机与 JSON 字符串的解析有什么关系呢?→ JSON 字符串是有格式规范的,比如 key 和 value 之间用冒号隔开,比如不同 key-value 对之间用逗号隔开…… 这些格式规范可以翻译成状态机的状态转移,比如 “如果检测到冒号,那么意味着下一步可以输入 value” 等等。还是以'{"a":"1", "b":2}'为例,我们来看看对这个 JSON 字符串进行解析时,状态机都流经了哪些状态。
    JSON.parse 三种实现方式 - 图4

    另外,这第三种实现方式,代码看起来非常的规整,是因为其广泛地应用了访问者模式,比如:

    1. var string = { // The actions for string tokens
    2. go: function () {
    3. state = "ok";
    4. },
    5. firstokey: function () {
    6. key = value;
    7. state = "colon";
    8. },
    9. okey: function () {
    10. key = value;
    11. state = "colon";
    12. },
    13. ovalue: function () {
    14. state = "ocomma";
    15. },
    16. firstavalue: function () {
    17. state = "acomma";
    18. },
    19. avalue: function () {
    20. state = "acomma";
    21. }
    22. };复制代码

    看似简单的 JSON.parse,要实现起来也是大有可究之处。如果想顺便看 JSON.stringify 的实现方法,可以看 Douglas Crockford 版,也可以看 MDN 版,两者大同小异。另外,鉴于 Douglas Crockford 写的这个 JSON 库有些特殊情况没处理好,后来又出了一个新的库,名为 JSON3,它们之间的区别详见相关的讨论