深入浅出JSON.parse

前言

众所周知,JSON.parse方法用于将一个json字符串转换成由字符串描述的 JavaScript 值或对象,该方法支持传入2个参数,第一个参数就是需要被转换的json字符串,第二个参数则是一个转换器函数(reviver,也叫还原函数),这个函数会针对每个键/值对都调用一次,这个转换器函数又接受2个参数,第一个参数为转换的每一个属性名,第二个参数则为转换的每一个属性值,并且该函数需要返回一个值,如果返回的是undefined,则结果中就会删除相应的键,如果返回了其他任何值,则该值就会成为相应键的值插入到结果中。

对于转换器函数更具体点讲就是:解析值本身以及它所包含的所有属性,会按照一定的顺序(从最最里层的属性开始,一级级往外,最终到达顶层,也就是解析值本身)分别的去调用 reviver 函数,在调用过程中,当前属性所属的对象会作为 this 值,当前属性名和属性值会分别作为第一个和第二个参数传入 reviver 中。如果 reviver 返回 undefined,则当前属性会从所属对象中删除,如果返回了其他值,则返回的值会成为当前属性新的属性值。

当遍历到最顶层的值(解析值)时,传入 reviver 函数的参数会是空字符串 ""(因为此时已经没有真正的属性)和当前的解析值(有可能已经被修改过了),当前的 this 值会是 {"": 修改过的解析值},在编写 reviver 函数时,要注意到这个特例。(这个函数的遍历顺序依照:从最内层开始,按照层级顺序,依次向外遍历)。

我们来看以下几个示例:

js 复制代码
const bool = JSON.parse('true'); // true
const obj = JSON.parse('{"k":1,"v":2}'); // { k:1 ,v: 2}
const obj2 = JSON.parse('{"k":1,"v":2}',(k,v) => {
    if(k === 'k'){
        return v + 2;
    }
    return v;
}); // { k:3 }
const obj3 = JSON.parse('{"k":1,"v":2}',(k,v) => {
    if(k === 'k'){
        return v + 2;
    }
    // 尤其需要注意这个特例
   if(k === ""){ return v; }
   return v + 1;
}); // { k:3:,v:3 }

实现方法

前面我们已经熟悉了该方法的使用方式,接下来,我们就根据该方法的使用方式来实现这个方法。在实现这个方法之前,我们需要知道一点,那就是想要解析出合格的JSON数据,那么数据格式就必须符合规定,例如:undefined不符合正确格式的数据格式,因此在实现的时候,我们都要将这种情况给考虑进去。

从前面的使用方式,我们不难看出,实际上整个解析过程就是在对整个json字符串进行遍历,而在遍历过程中我们就需要针对不同的数据类型做不同的处理,例如如果是解析字符串,我们只需要创建一个空字符串,遍历字符串每一个字符,然后将字符拼接起来即可,当然在遍历过程中,我们还需要对一些特殊字符或者符号进行处理。

理解了整体的思路,接下来,我们就来一步一步的实现这个方法吧。

创建一个自调用函数

我们采用的是创建一个自调用函数,并且这个函数返回一个函数,而在这个返回的函数当中,我们会提供2个参数,正如前面所介绍的那样,这2个参数分别是被解析的json字符串和转换器函数,命名为source与reviver,代码如下所示:

js 复制代码
const jsonParser = (() => {
    // ...
    return (source,reviver) => {
        // ...
    }
})();

这个函数内部,我们将会定义一个变量result,用来存储最终的解析结果,并且我们会根据第二个参数reviver是否是一个函数来确定是直接返回这个结果还是返回reviver转换器函数,这个转换器函数也是一个自调用函数,由于我们的json数据可能是嵌套的对象或者数组,因此这里我们也需要定义一个函数名,方便递归调用。

一些变量的定义

这里的实现我们最后来说,接下来我们需要先定义一些变量,比如当前字符索引值,当前字符,一些特殊字符的定义,以及需要一个变量来缓存原始json字符串,同样的如果在解析字符串时出现不符合规定的字符,则需要提示错误,因此我们也会封装一个error函数,如下所示:

js 复制代码
const jsonParser = (() => {
    let at, // 当前字符索引值
        ch, // 当前遍历字符
        text, // 缓存原始json字符串
        escapee = {
          '"': '"',
          '\\': '\\',
          '/': '/',
          b: 'b',
          f: '\f',
          n: '\n',
          r: '\r',
          t: '\t'
        }, // 特殊字符
        error = m => {
          const errorObj = {
                name: 'SyntaxError',
                message: m,
                at,
                text
          };
          console.error(`${JSON.stringify(errorObj)}`); // 控制台打印错误
          // 或者使用throw抛出错误,即
          // throw errorObj;
        },
        // ...
    return (source,reviver) => {
        // ...
    }
})();

next方法

接下来,我们还需要实现一个next方法,这个方法,在这个方法中,我们会依次的去读取字符串的每一个字符,并将索引值加1,读取字符我们可以使用String.charAt方法,该方法就是读取字符串的每一个字符。如:

js 复制代码
const str = "hello";
str.charAt(0); // h

需要注意的就是该方法支持传入一个参数,并且如果传入的参数,不等于我们的字符ch,则需要抛出一个两者不相等的错误。

有了以上的分析,我们的next方法就很好实现了,如下所示:

js 复制代码
const jsonParser = (() => {
    let at, 
        //...
        next = c => {
            // 如果有传入参数,并且该参数值不等于当前字符,则需要给出错误提示
            if(c && c !== ch){
                error(`预期${c}代替${ch}`);
            }
            // 根据索引值读取当前字符
            ch = text.charAt(at);
            at++; // 索引值加1
            //返回当前字符
            return ch;
        },
        //...
    return (source,reviver) => {
        // ...
    }
})();

依据数据类型解析

接下来,我们就要根据当前解析的json字符串属于哪一种数据类型而依次去解析了,数据类型主要分为数值number,字符串string,布尔值boolean和null,以及对象object和数组,当然还有空格字符。

也许有人好奇为什么会没有undefined类型,我们来看如下图所示:

如上图所示,JSON.parse是不能够解析undefined的,当然如果"undefined"字符串是作为一个对象的属性值,还是可以被解析出来的,如果是undefined作为属性值,是不会被解析出来的。如:

js 复制代码
JSON.parse('{"a":"undefined"}'); // { a:"undefined" }
JSON.parse('{"a":undefined}'); // Unexpected token 'u', "{"a":undefined}" is not valid JSON
// 数组解析同理

布尔值,null与空白字符的解析

我们先来看最简单的两种数据类型的解析,由于布尔值和null两者解析过程相似,因此归为一类定义一个方法来解析,空白字符只需要跳过即可。代码如下所示:

js 复制代码
const jsonParser = (() => {
    let at, 
        //...
        white = () => {
            // 注意这里为什么是使用<=而非==,因为还有类似\n这样的空白符
            while(ch && ch <= ' '){
                next();
            }
        },
        word = () => {
            switch (ch) {
                case 't':
                  next('t');
                  next('r');
                  next('u');
                  next('e');
                return true;
                case 'f':
                  next('f');
                  next('a');
                  next('l');
                  next('s');
                  next('e');
                return false;
                case 'n':
                  next('n');
                  next('u');
                  next('l');
                  next('l');
                return null;
          };
          error(`意料之外的值:${ch}`);
        },
        //...
    return (source,reviver) => {
        // ...
    }
})();

可以看到解析布尔值和null,我们只需要根据首字符是否为该类型数据的首字母即可判定解析,然后将结果返回出去即可,如果不满足条件,则需要报错。

数值的解析

接下来我们来看数值类型数据的解析,数值类型我们需要考虑四种情况,第一种就是正负号的解析,第二种则是e字母的解析(即科学计数法),第三种则是小数点'.'的解析,最后一种则是数字的解析。在该方法内部,我们将创建2个变量,因为虽然是数值数据,但是我们是一个字符一个字符的解析,而非做计算,因此就需要拼接字符串,不过拼接完之后的字符串我们需要转换成数字,这两个变量就做这2个工作的。

首先我们需要判断是否为负号,从而直接拼接,然后继续下一个字符,下一个字符我们需要将负号当做参数传给next方法,接着我们循环当前字符是否是数字,如何判断是否是数字呢?我们只需要比较是否大于等于0并且小于等于9即可,注意这里我们比较的是字符串的码序,而不是单纯的比数字大小来判定是否是数字。即:

js 复制代码
const isNumber = v => v >= '0' && v <= '9';

拼接数字完成之后,我们还要继续调用next方法进行下一步,注意这里调用不需要传任何参数。

完成数字的拼接之后,我们接着判断是否是小数点从而继续拼接,小数点之后会继续是数字,因此我们还要继续循环数字从而继续拼接。

最后一步就是判断当前字符是否是e字母,注意e字母不区分大小写,因此需要两个判断条件,e字母后面也有可能有正负号,因此也需要判断是否是正负号,正负号后面还会有数字,也需要继续拼接,从而调用next方法进行下一步。

最后把拼接后的字符串利用加号操作符转换成数值,从而得到最终的结果,最终的结果有可能是一个NaN,因此我们还需要判断一下是否是NaN,如果是NaN,则给出一个错误提示,否则直接返回最终的结果。

根据以上的分析,最终我们的number转换方法如下所示:

js 复制代码
const jsonParser = (() => {
    let at, 
        //...
        number = () => {
            let number,string = ''; // 定义number存储最终转换成数字的结果,定义string变量拼接字符串
            // 符号的拼接,+号通常是不会写的,因此不需要判断
            if(ch === '-'){
                string += ch;
                next('-');
            }
            // 循环数字
            while(ch >= '0' && ch <= '9'){
                string += ch;
                next();
            }
            //小数点
            if(ch === '.'){
                string += ch;
                while(next() && ch >= '0' && ch <= '9'){
                    string += ch;
                    next();
                }
            }
            //科学计数法
            if(ch === 'e' || ch === 'E'){
                string += ch;
                next();
                // 科学计数法e字母后还有可能是正负号
                if(ch === '-' || ch === '+'){
                    string += ch;
                    next();
                }
                // 科学计数法e字母之后的数字
                while(ch >= '0' && ch <= '9'){
                    string += ch;
                    next();
                }
            }
            
          // 转换成数值赋值给number变量
          number = +string;
          //判断是否是NaN
          if(isNaN(number)){
              error('错误的数值');
          }else{
              return number;
          }
     },
    //...
    return (source,reviver) => {
        // ...
    }
})();

字符串的解析

数值类型解析完成,接下来我们来看字符串的解析,字符串的解析也是需要分情况的,首先是Unicode字符,即以u字母开头的字符,最准确的说应该是类似这样的unicode字符串'\u2233'的解析。遇到这样的字符,我们会使用String.fromCharCode方法转换成普通的字符串,这里的转换也涉及到了一个转换公式原理,我们会使用parseInt将其转换成16进制的数值,然后将该数字乘以16,并相加,初始结果为0,我们会定义一个变量uChar来用作计算后的结果。

首先第一步,我们知道字符串以"开头,因此首先我们需要判断是否是",最开始我们也需要定义4个变量,即hex,i,uChar,string,其中hex用来存储parseInt转换成16进制后的结果,uChar用来存储最终的转换结果,i就是循环变量,string则是最终拼接出来的结果。

判断完成之后,我们将依次循环下一个字符,在循环当中,如果遇到另一个",则代表字符串已经拼接完成,直接返回string结果,并退出循环,否则遇到当前字符是"\\",则需要将unicode字符进行转换,首先还是调用next方法跳过该字符,然后判断是否是u字母或者我们定义好的escapee中的特殊字符,如果两者都不是,则需要跳出循环,最后将String.fromCharCode方法转换uChar的结果值拼接给结果变量string。

这其中额外需要注意的就是Unicode字符的计算,我们会以4为循环最终条件,去计算,并且我们在循环当中还会判断是否是一个有限的数值,从而决定是否跳出该循环。

否则就是直接字符串拼接直到循环完成,如果不满足相应的条件,我们最终也会给出错误提示。根据以上分析,最终我们拼接字符串的代码如下所示:

js 复制代码
const jsonParser = (() => {
    let at, 
        //...
        string = () => {
            let hex,i,string,uChar;
            if(ch === '"'){
                // 从下一个字符开始循环
                while(next()){
                    if(ch === '"'){
                        // 如果是另一个双引号,则是字符串的结束
                        next();
                        return string;
                    }else if(ch === '\\'){
                        // 如果是Unicode字符
                        next();
                        // 如果当前字符是u字母
                        if(ch === 'u'){
                            uChar = 0;
                            for(i = 0;i < 4;i++){
                                // 转换成16进制数
                                hex = parseInt(next(),16);
                                // 如果hex不是一个有限数值,则跳出循环
                                if(!isFinite(hex)){
                                    break;
                                }
                                // 计算uChar
                                uChar = uChar * 16 + hex;
                            }
                        }else if(typeof escapee[ch] === 'string'){
                            // 如果是特殊字符,则直接拼接
                            string += escapee[ch];
                        }else{
                            // 跳出循环
                            break;
                        }
                        // 拼接最终结果
                        string += String.fromCharCode(uChar);
                    }else{
                        // 否则当成普通字符拼接
                        string += ch;
                    }
                }
            }
            // 如果当前字符不是"开头,则是一个错误的字符串
            error('错误的字符串');
        },
        //...
    return (source,reviver) => {
        // ...
    }
})();

数组的解析

字符串和数值以及布尔值还有null都解析完了,接下来就是数组和对象的解析了,我们先来看数组的解析。数组一定是以"["开头的,而它里面的值有可能是字符串,或者数组或者对象等,因此在这之前我们需要先定义一个值变量value用来存储这种不可推测的值,如下所示:

js 复制代码
const jsonParser = (() => {
    let at, 
        //...
        value,
        //...
    return (source,reviver) => {
        // ...
    }
})();

数组的解析也不复杂,我们还是会定义一个array变量用来缓存最终的结果,接着判断是否以[开头,如果是就继续下一个字符,并且有可能该字符后面有空白,因此我们需要调用white方法,紧接着我们判断下一个字符是否是],如果是,就代表数组解析已结束,直接返回array结果。

否则循环当前字符,并将值(也就是我们定义的value变量)添加到array中,然后再调用一次white方法跳过空白字符,紧接着判断是否是]字符,如果是就继续下一个字符的遍历,并返回结果,否则将逗号当做参数传给next方法,当做下一个字符的遍历,然后再调用一次white方法跳过空白字符。

否则最后我们就给出一个错误提示,错误的数组。根据以上的分析,最终可得代码如下所示:

js 复制代码
const jsonParser = (() => {
    let at, 
        //...
        value,
        array = () => {
            const array = [];
            // [开头则继续下一个字符,并跳过空白
            if(ch === '['){
                next('[');
                white();
            }
            // ]则解析结束,返回结果
            if(ch === ']'){
                next(']');
                return array;
            }
            // 循环字符
            while(ch){
                array.push(value());
                white();
                // ]则结束解析
                if(ch === ']'){
                    next(']');
                    return array;
                }
                // 跳过逗号字符的解析
                next(',');
                white();
            }
            // 错误的数组数据
            error('错误的数组');
        }
        //...
    return (source,reviver) => {
        // ...
    }
})();

对象的解析

对象的解析与数组的解析有些类似,不过对象需要考虑属性名和属性值,属性名实际上就是对字符串的解析,而属性值则与数组项一样,是不可推测的value值,遇到:字符,我们也需要跳过,并解析下一个字符。

中间可能也会有空白字符,因此需要跳过,我们会创建2个变量,第一个变量用于缓存属性名,第二个变量则是存储结果值,我们知道对象是"{"开始,"}"结束的,除了这些需要注意的地方,其它就和解析数组一样差不多了。

根据以上的分析,我们最终的代码如下所示:

js 复制代码
const jsonParser = (() => {
    let at, 
        //...
        value,
        object = () => {
            let key,object = {}; // 存储属性名和结果所定义的变量
            if(ch === '{'){
                // 跳过{字符解析下一个字符
                next('{');
                // 可能存在空白字符
                white();
                // 如果是}字符,则结束解析,并返回结果
                if(ch === '}'){
                    next('}');
                    return object;
                }
                // 循环字符
                while(ch){
                    // 属性名即解析字符串
                    key = string();
                    // 可能存在空白字符,跳过
                    white();
                    // 跳过:字符
                    next(':');
                    // value值是一个函数,下文会介绍
                    object[key] = value();
                    // 跳过空白
                    white();
                    // 如果是},则解析结束
                    if(ch === '}'){
                        next('}');
                        return object;
                    }
                    // 跳过,字符解析下一个字符
                    next(',');
                    // 可能存在空白字符,跳过
                    white();
                }
            }
            // 如果不是以{开头,则对象格式不符合,抛出错误
            error('错误的对象');
        },
        //...
    return (source,reviver) => {
        // ...
    }
})();

不可推测的值

前文也提到了不可推测的值value,它可以是数组,对象,字符串,数值,布尔值,null等其中的一个,因此该值我们定义成一个函数,并根据当前字符以什么开头来确定数据类型,从而决定使用哪个方法解析,比如是字符串,就会以"开头,从而调用前面实现的string方法进行解析,如果是数组对象等同理,默认当然是以数值和布尔值以及null解析为主。

当然最开始可能也会有空白字符,需要跳过,根据以上的分析,value函数最终代码如下所示:

js 复制代码
const jsonParser = (() => {
    let at, 
        //...
        value,
        //...定义完object方法之后再赋值value
        value = () => {
            // 可能存在空白字符,跳过
            white();
            // 判断以什么字符开头
            switch(ch){
                case '{':
                    return object(); // 对象解析
                case '[':
                    return array(); // 数组解析
                case '"':
                    return string(); // 字符串解析
                case '-':
                    return number(); // 数值解析
                default:
                    return ch >= '0' && ch <= '9' ? number() : word(); // 如果是数字则当做是数值解析,否则当做布尔值或null解析
            }
        };
    return (source,reviver) => {
        // ...
    }
})();

返回结果:有转换器函数与无转换器函数

最后我们来看返回的函数的实现原理,首先我们创建了4个变量,result,text = source,at = 0,ch = ' ',分别代表最终的解析结果,原始json字符串,起始解析索引值,从0开始,起始解析字符,从空白字符开始。

接着调用value方法解析值,并赋值给结果变量result,然后调用white方法跳过空白字符,跳过空白字符之后,如果还存在字符未解析,就代表解析数据不是一个合格的json字符串,则给出错误提示。

最后函数结果返回2个结果,第一个结果就是如果传入了转换器函数,则返回一个自调用函数,否则返回result。如下所示:

js 复制代码
const jsonParser = (() => {
    // ...
    return (source,reviver) => {
        // 解析结果,原始字符串,起始解析索引值,起始解析字符
        let result,text = source,at = 0,ch = ' ';
        // 解析值并赋值
        result = value();
        // 跳过空白字符
        white();
        // 如果还存在解析字符,则数据不符合json规范,给出错误
        if(ch){
            error('解析语法错误,不是一个合格的json数据');
        }
        // 返回
        return typeof reviver === 'function' ? (function walk(holder,key){
            // ...
        })({ '':result },'') : result;
    }
})();

转换器内部的实现原理

还记得前面有一个这样的示例,如下所示:

js 复制代码
const obj3 = JSON.parse('{"k":1,"v":2}',(k,v) => {
    if(k === 'k'){
        return v + 2;
    }
    // 尤其需要注意这个特例
   if(k === ""){ return v; }
   return v + 1;
}); // { k:3:,v:3 }

从以上特例,我们可以得知最开始会以一个空属性名作为遍历的开始,这也是为什么我们的自调用函数的第一个参数值是{ '':result }的原因,第二个参数也是以空属性名作为遍历开始的。

在递归函数walk内部,我们会定义3个变量,即循环属性名k,缓存的属性值v,和起始属性值value = holder[key]。起始属性值实际上就是原始解析结果开始,如果该值是一个对象,我们则需要遍历该对象,如果我们的循环属性名k是该对象的属性,则递归的赋值缓存属性值,然后判断属性值如果是undefined,则从对象中删除该属性,否则修改该属性值,最终我们会返回调用转换器函数的结果。

根据以上代码分析,我们最终转换器函数内部实现原理代码如下所示:

js 复制代码
const jsonParser = (() => {
    // ...
    return (source,reviver) => {
        // ...
        return typeof reviver === 'function' ? (function walk(holder,key){
            // 循环属性名,缓存属性值,读取值
            let k,v,value = holder[key];
            // 如果值是对象,则需要继续解析
            if(value && typeof value === 'object'){
                // 循环对象属性值
                for(k in value){
                    // 如果value中存在该属性
                    if(Object.hasOwnProperty.call(value,k)){
                        // 继续递归
                        v = walk(value,k);
                        // 如果属性值不是undefined则修改属性值,否则删除该属性
                        if(v !== undefined){
                            value[k] = v;
                        }else{
                            delete value[k];
                        }
                    }
                }
            }
            // 返回转换器函数调用的结果
            return reviver.call(holder,key,value);
        })({ '':result },'') : result;
    }
})();

最终

将以上代码整合起来,得到了我们的parse解析方法的实现,以上源码可以查看这里

感谢大家的阅读,本文如有错误,敬请指正,如果有用,望不吝啬点赞和收藏。

相关推荐
程序员凡尘21 分钟前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js
编程零零七4 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
(⊙o⊙)~哦6 小时前
JavaScript substring() 方法
前端
无心使然云中漫步6 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者6 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_7 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
麒麟而非淇淋8 小时前
AJAX 入门 day1
前端·javascript·ajax
2401_858120538 小时前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢8 小时前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写9 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js