前言
众所周知,JSON.parse
方法用于将一个json
字符串转换成由字符串描述的 JavaScript 值或对象,该方法支持传入2个参数,第一个参数就是需要被转换的json
字符串,第二个参数则是一个转换器函数(reviver,也叫还原函数),这个函数会针对每个键/值对都调用一次,这个转换器函数又接受2个参数,第一个参数为转换的每一个属性名,第二个参数则为转换的每一个属性值,并且该函数需要返回一个值,如果返回的是undefined,则结果中就会删除相应的键,如果返回了其他任何值,则该值就会成为相应键的值插入到结果中。
对于转换器函数更具体点讲就是:解析值本身以及它所包含的所有属性,会按照一定的顺序(从最最里层的属性开始,一级级往外,最终到达顶层,也就是解析值本身)分别的去调用 reviver
函数,在调用过程中,当前属性所属的对象会作为 this
值,当前属性名和属性值会分别作为第一个和第二个参数传入 reviver
中。如果 reviver
返回 undefined
,则当前属性会从所属对象中删除,如果返回了其他值,则返回的值会成为当前属性新的属性值。
当遍历到最顶层的值(解析值)时,传入 reviver
函数的参数会是空字符串 ""
(因为此时已经没有真正的属性)和当前的解析值(有可能已经被修改过了),当前的 this
值会是 {"": 修改过的解析值}
,在编写 reviver
函数时,要注意到这个特例。(这个函数的遍历顺序依照:从最内层开始,按照层级顺序,依次向外遍历)。
我们来看以下几个示例:
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,代码如下所示:
const jsonParser = (() => {
// ...
return (source,reviver) => {
// ...
}
})();
这个函数内部,我们将会定义一个变量result,用来存储最终的解析结果,并且我们会根据第二个参数reviver是否是一个函数来确定是直接返回这个结果还是返回reviver转换器函数,这个转换器函数也是一个自调用函数,由于我们的json数据可能是嵌套的对象或者数组,因此这里我们也需要定义一个函数名,方便递归调用。
一些变量的定义
这里的实现我们最后来说,接下来我们需要先定义一些变量,比如当前字符索引值,当前字符,一些特殊字符的定义,以及需要一个变量来缓存原始json字符串,同样的如果在解析字符串时出现不符合规定的字符,则需要提示错误,因此我们也会封装一个error函数,如下所示:
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
方法,该方法就是读取字符串的每一个字符。如:
const str = "hello";
str.charAt(0); // h
需要注意的就是该方法支持传入一个参数,并且如果传入的参数,不等于我们的字符ch,则需要抛出一个两者不相等的错误。
有了以上的分析,我们的next方法就很好实现了,如下所示:
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作为属性值,是不会被解析出来的。如:
JSON.parse('{"a":"undefined"}'); // { a:"undefined" }
JSON.parse('{"a":undefined}'); // Unexpected token 'u', "{"a":undefined}" is not valid JSON
// 数组解析同理
布尔值,null与空白字符的解析
我们先来看最简单的两种数据类型的解析,由于布尔值和null两者解析过程相似,因此归为一类定义一个方法来解析,空白字符只需要跳过即可。代码如下所示:
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即可,注意这里我们比较的是字符串的码序,而不是单纯的比数字大小来判定是否是数字。即:
const isNumber = v => v >= '0' && v <= '9';
拼接数字完成之后,我们还要继续调用next方法进行下一步,注意这里调用不需要传任何参数。
完成数字的拼接之后,我们接着判断是否是小数点从而继续拼接,小数点之后会继续是数字,因此我们还要继续循环数字从而继续拼接。
最后一步就是判断当前字符是否是e字母,注意e字母不区分大小写,因此需要两个判断条件,e字母后面也有可能有正负号,因此也需要判断是否是正负号,正负号后面还会有数字,也需要继续拼接,从而调用next方法进行下一步。
最后把拼接后的字符串利用加号操作符转换成数值,从而得到最终的结果,最终的结果有可能是一个NaN,因此我们还需要判断一下是否是NaN,如果是NaN,则给出一个错误提示,否则直接返回最终的结果。
根据以上的分析,最终我们的number转换方法如下所示:
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为循环最终条件,去计算,并且我们在循环当中还会判断是否是一个有限的数值,从而决定是否跳出该循环。
否则就是直接字符串拼接直到循环完成,如果不满足相应的条件,我们最终也会给出错误提示。根据以上分析,最终我们拼接字符串的代码如下所示:
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用来存储这种不可推测的值,如下所示:
const jsonParser = (() => {
let at,
//...
value,
//...
return (source,reviver) => {
// ...
}
})();
数组的解析也不复杂,我们还是会定义一个array变量用来缓存最终的结果,接着判断是否以[
开头,如果是就继续下一个字符,并且有可能该字符后面有空白,因此我们需要调用white方法,紧接着我们判断下一个字符是否是]
,如果是,就代表数组解析已结束,直接返回array结果。
否则循环当前字符,并将值(也就是我们定义的value变量)添加到array中,然后再调用一次white方法跳过空白字符,紧接着判断是否是]
字符,如果是就继续下一个字符的遍历,并返回结果,否则将逗号当做参数传给next方法,当做下一个字符的遍历,然后再调用一次white方法跳过空白字符。
否则最后我们就给出一个错误提示,错误的数组。根据以上的分析,最终可得代码如下所示:
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个变量,第一个变量用于缓存属性名,第二个变量则是存储结果值,我们知道对象是"{"
开始,"}"
结束的,除了这些需要注意的地方,其它就和解析数组一样差不多了。
根据以上的分析,我们最终的代码如下所示:
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函数最终代码如下所示:
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。如下所示:
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;
}
})();
转换器内部的实现原理
还记得前面有一个这样的示例,如下所示:
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,则从对象中删除该属性,否则修改该属性值,最终我们会返回调用转换器函数的结果。
根据以上代码分析,我们最终转换器函数内部实现原理代码如下所示:
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解析方法的实现,以上源码可以查看这里。
在线效果如下:
感谢大家的阅读,本文如有错误,敬请指正,如果有用,望不吝啬点赞和收藏。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。