前言
众所周知,JSON.parse
方法用于将一个json
字符串转换成由字符串描述的 JavaScript 值或对象,该方法支持传入2个参数,第一个参数就是需要被转换的json
字符串,第二个参数则是一个转换器函数(reviver,也叫还原函数),这个函数会针对每个键/值对都调用一次,这个转换器函数又接受2个参数,第一个参数为转换的每一个属性名,第二个参数则为转换的每一个属性值,并且该函数需要返回一个值,如果返回的是undefined,则结果中就会删除相应的键,如果返回了其他任何值,则该值就会成为相应键的值插入到结果中。
对于转换器函数更具体点讲就是:解析值本身以及它所包含的所有属性,会按照一定的顺序(从最最里层的属性开始,一级级往外,最终到达顶层,也就是解析值本身)分别的去调用 reviver
函数,在调用过程中,当前属性所属的对象会作为 this
值,当前属性名和属性值会分别作为第一个和第二个参数传入 reviver
中。如果 reviver
返回 undefined
,则当前属性会从所属对象中删除,如果返回了其他值,则返回的值会成为当前属性新的属性值。
当遍历到最顶层的值(解析值)时,传入 reviver
函数的参数会是空字符串 ""
(因为此时已经没有真正的属性)和当前的解析值(有可能已经被修改过了),当前的 this
值会是 {"": 修改过的解析值}
,在编写 reviver
函数时,要注意到这个特例。(这个函数的遍历顺序依照:从最内层开始,按照层级顺序,依次向外遍历)。
我们来看以下几个示例:
1 | js复制代码const bool = JSON.parse('true'); // true |
实现方法
前面我们已经熟悉了该方法的使用方式,接下来,我们就根据该方法的使用方式来实现这个方法。在实现这个方法之前,我们需要知道一点,那就是想要解析出合格的JSON数据,那么数据格式就必须符合规定,例如:undefined不符合正确格式的数据格式,因此在实现的时候,我们都要将这种情况给考虑进去。
从前面的使用方式,我们不难看出,实际上整个解析过程就是在对整个json字符串进行遍历,而在遍历过程中我们就需要针对不同的数据类型做不同的处理,例如如果是解析字符串,我们只需要创建一个空字符串,遍历字符串每一个字符,然后将字符拼接起来即可,当然在遍历过程中,我们还需要对一些特殊字符或者符号进行处理。
理解了整体的思路,接下来,我们就来一步一步的实现这个方法吧。
创建一个自调用函数
我们采用的是创建一个自调用函数,并且这个函数返回一个函数,而在这个返回的函数当中,我们会提供2个参数,正如前面所介绍的那样,这2个参数分别是被解析的json字符串和转换器函数,命名为source与reviver,代码如下所示:
1 | js复制代码const jsonParser = (() => { |
这个函数内部,我们将会定义一个变量result,用来存储最终的解析结果,并且我们会根据第二个参数reviver是否是一个函数来确定是直接返回这个结果还是返回reviver转换器函数,这个转换器函数也是一个自调用函数,由于我们的json数据可能是嵌套的对象或者数组,因此这里我们也需要定义一个函数名,方便递归调用。
一些变量的定义
这里的实现我们最后来说,接下来我们需要先定义一些变量,比如当前字符索引值,当前字符,一些特殊字符的定义,以及需要一个变量来缓存原始json字符串,同样的如果在解析字符串时出现不符合规定的字符,则需要提示错误,因此我们也会封装一个error函数,如下所示:
1 | js复制代码const jsonParser = (() => { |
next方法
接下来,我们还需要实现一个next方法,这个方法,在这个方法中,我们会依次的去读取字符串的每一个字符,并将索引值加1,读取字符我们可以使用String.charAt
方法,该方法就是读取字符串的每一个字符。如:
1 | js复制代码const str = "hello"; |
需要注意的就是该方法支持传入一个参数,并且如果传入的参数,不等于我们的字符ch,则需要抛出一个两者不相等的错误。
有了以上的分析,我们的next方法就很好实现了,如下所示:
1 | js复制代码const jsonParser = (() => { |
依据数据类型解析
接下来,我们就要根据当前解析的json字符串属于哪一种数据类型而依次去解析了,数据类型主要分为数值number,字符串string,布尔值boolean和null,以及对象object和数组,当然还有空格字符。
也许有人好奇为什么会没有undefined类型,我们来看如下图所示:
如上图所示,JSON.parse
是不能够解析undefined的,当然如果”undefined”字符串是作为一个对象的属性值,还是可以被解析出来的,如果是undefined作为属性值,是不会被解析出来的。如:
1 | js复制代码JSON.parse('{"a":"undefined"}'); // { a:"undefined" } |
布尔值,null与空白字符的解析
我们先来看最简单的两种数据类型的解析,由于布尔值和null两者解析过程相似,因此归为一类定义一个方法来解析,空白字符只需要跳过即可。代码如下所示:
1 | js复制代码const jsonParser = (() => { |
可以看到解析布尔值和null,我们只需要根据首字符是否为该类型数据的首字母即可判定解析,然后将结果返回出去即可,如果不满足条件,则需要报错。
数值的解析
接下来我们来看数值类型数据的解析,数值类型我们需要考虑四种情况,第一种就是正负号的解析,第二种则是e字母的解析(即科学计数法),第三种则是小数点’.’的解析,最后一种则是数字的解析。在该方法内部,我们将创建2个变量,因为虽然是数值数据,但是我们是一个字符一个字符的解析,而非做计算,因此就需要拼接字符串,不过拼接完之后的字符串我们需要转换成数字,这两个变量就做这2个工作的。
首先我们需要判断是否为负号,从而直接拼接,然后继续下一个字符,下一个字符我们需要将负号当做参数传给next方法,接着我们循环当前字符是否是数字,如何判断是否是数字呢?我们只需要比较是否大于等于0并且小于等于9即可,注意这里我们比较的是字符串的码序,而不是单纯的比数字大小来判定是否是数字。即:
1 | js复制代码const isNumber = v => v >= '0' && v <= '9'; |
拼接数字完成之后,我们还要继续调用next方法进行下一步,注意这里调用不需要传任何参数。
完成数字的拼接之后,我们接着判断是否是小数点从而继续拼接,小数点之后会继续是数字,因此我们还要继续循环数字从而继续拼接。
最后一步就是判断当前字符是否是e字母,注意e字母不区分大小写,因此需要两个判断条件,e字母后面也有可能有正负号,因此也需要判断是否是正负号,正负号后面还会有数字,也需要继续拼接,从而调用next方法进行下一步。
最后把拼接后的字符串利用加号操作符转换成数值,从而得到最终的结果,最终的结果有可能是一个NaN,因此我们还需要判断一下是否是NaN,如果是NaN,则给出一个错误提示,否则直接返回最终的结果。
根据以上的分析,最终我们的number转换方法如下所示:
1 | js复制代码const jsonParser = (() => { |
字符串的解析
数值类型解析完成,接下来我们来看字符串的解析,字符串的解析也是需要分情况的,首先是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为循环最终条件,去计算,并且我们在循环当中还会判断是否是一个有限的数值,从而决定是否跳出该循环。
否则就是直接字符串拼接直到循环完成,如果不满足相应的条件,我们最终也会给出错误提示。根据以上分析,最终我们拼接字符串的代码如下所示:
1 | js复制代码const jsonParser = (() => { |
数组的解析
字符串和数值以及布尔值还有null都解析完了,接下来就是数组和对象的解析了,我们先来看数组的解析。数组一定是以"["
开头的,而它里面的值有可能是字符串,或者数组或者对象等,因此在这之前我们需要先定义一个值变量value用来存储这种不可推测的值,如下所示:
1 | js复制代码const jsonParser = (() => { |
数组的解析也不复杂,我们还是会定义一个array变量用来缓存最终的结果,接着判断是否以[
开头,如果是就继续下一个字符,并且有可能该字符后面有空白,因此我们需要调用white方法,紧接着我们判断下一个字符是否是]
,如果是,就代表数组解析已结束,直接返回array结果。
否则循环当前字符,并将值(也就是我们定义的value变量)添加到array中,然后再调用一次white方法跳过空白字符,紧接着判断是否是]
字符,如果是就继续下一个字符的遍历,并返回结果,否则将逗号当做参数传给next方法,当做下一个字符的遍历,然后再调用一次white方法跳过空白字符。
否则最后我们就给出一个错误提示,错误的数组。根据以上的分析,最终可得代码如下所示:
1 | js复制代码const jsonParser = (() => { |
对象的解析
对象的解析与数组的解析有些类似,不过对象需要考虑属性名和属性值,属性名实际上就是对字符串的解析,而属性值则与数组项一样,是不可推测的value值,遇到:
字符,我们也需要跳过,并解析下一个字符。
中间可能也会有空白字符,因此需要跳过,我们会创建2个变量,第一个变量用于缓存属性名,第二个变量则是存储结果值,我们知道对象是"{"
开始,"}"
结束的,除了这些需要注意的地方,其它就和解析数组一样差不多了。
根据以上的分析,我们最终的代码如下所示:
1 | js复制代码const jsonParser = (() => { |
不可推测的值
前文也提到了不可推测的值value,它可以是数组,对象,字符串,数值,布尔值,null等其中的一个,因此该值我们定义成一个函数,并根据当前字符以什么开头来确定数据类型,从而决定使用哪个方法解析,比如是字符串,就会以"
开头,从而调用前面实现的string方法进行解析,如果是数组对象等同理,默认当然是以数值和布尔值以及null解析为主。
当然最开始可能也会有空白字符,需要跳过,根据以上的分析,value函数最终代码如下所示:
1 | js复制代码const jsonParser = (() => { |
返回结果:有转换器函数与无转换器函数
最后我们来看返回的函数的实现原理,首先我们创建了4个变量,result,text = source,at = 0,ch = ‘ ‘,分别代表最终的解析结果,原始json字符串,起始解析索引值,从0开始,起始解析字符,从空白字符开始。
接着调用value方法解析值,并赋值给结果变量result,然后调用white方法跳过空白字符,跳过空白字符之后,如果还存在字符未解析,就代表解析数据不是一个合格的json字符串,则给出错误提示。
最后函数结果返回2个结果,第一个结果就是如果传入了转换器函数,则返回一个自调用函数,否则返回result。如下所示:
1 | js复制代码const jsonParser = (() => { |
转换器内部的实现原理
还记得前面有一个这样的示例,如下所示:
1 | js复制代码const obj3 = JSON.parse('{"k":1,"v":2}',(k,v) => { |
从以上特例,我们可以得知最开始会以一个空属性名作为遍历的开始,这也是为什么我们的自调用函数的第一个参数值是{ '':result }
的原因,第二个参数也是以空属性名作为遍历开始的。
在递归函数walk内部,我们会定义3个变量,即循环属性名k,缓存的属性值v,和起始属性值value = holder[key]
。起始属性值实际上就是原始解析结果开始,如果该值是一个对象,我们则需要遍历该对象,如果我们的循环属性名k是该对象的属性,则递归的赋值缓存属性值,然后判断属性值如果是undefined,则从对象中删除该属性,否则修改该属性值,最终我们会返回调用转换器函数的结果。
根据以上代码分析,我们最终转换器函数内部实现原理代码如下所示:
1 | js复制代码const jsonParser = (() => { |
最终
将以上代码整合起来,得到了我们的parse解析方法的实现,以上源码可以查看这里。
感谢大家的阅读,本文如有错误,敬请指正,如果有用,望不吝啬点赞和收藏。
本文转载自: 掘金