JSON.stringify是将对象字符串化,一般用在前后端数据传递,用法也是非常的简单,例如:JSON.stringify({name:'aaa'})就得到了"{"name":"aaa"}",也就是常说的JSON字符串。但是如果这个对象里面存在循环引用的话,就会比较麻烦,如下:
- let a={name:'aaa',link:''}
- let b={name:'bbb',link:''}
- a.link=b;
- b.link=a;
- /*运行*/
- JSON.stringify(a);
- /*报错*/
- VM3272:1 Uncaught TypeError: Converting circular structure to JSON
我们可以发现此时JSON.stringify会报错。此时,可以用JSON-js去破除这种循环结构。
基本用法
JSON-js是老外写的一个对JSON处理的小工具,其中的decycle和retrocycle是专门用来破除/恢复这种循环结构的。
下面是包含decycle和retrocycle的核心实现的js代码:
先来看下decycle最简单的用法:
- let a={name:'aaa',link:''}
- let b={name:'bbb',link:''}
- a.link=b;
- b.link=a;
- /*decycle*/
- JSON.stringify(JSON.decycle(a));
- /*结果*/
- "{"name":"aaa","link":{"name":"bbb","link":{"$ref":"$"}}}"
我们可以看到,破解循环后确实没有报错,但是出现了$ref:'$'这样的代码,这种标志表示识别除了循环引用,其中$ref为固定的,右边的'$...'表示它循环引用的部分,单个$为顶层对象,我们现在看个不是顶层对象的情况:
- let a={name:'aaa',link:{url:[{href:''}]}}
- let b={name:'bbb',link:{url:[{href:''}]}}
- a.link.url[0].href=b.link.url[0];
- b.link.url[0].href=a.link.url[0];
- /*执行*/
- JSON.stringify(JSON.decycle(a));
- /*结果*/
- "{"name":"aaa","link":{"url":[{"href":{"href":{"$ref":"$[\"link\"][\"url\"][0]"}}}]}}"
我们看到现在为$ref:$['link']['url'][0],我们画个示意图:
我们从对象a开始遍历,依次会经过节点A->节点B->线路1->节点C->节点D->线路2->节点A->.....,我们发现,经过线路2之后,路线就会发生重复,因此$ref:$['link']['url'][0]的意思就是表示在a['link']['url'][0]处发生了循环,因此不再遍历,只保留一个记号。
JSON.retrocycle则是恢复破解,识别key为$ref的,然后恢复引用。
- let a={name:'aaa',link:''}
- let b={name:'bbb',link:''}
- a.link=b;
- b.link=a;
- /*破解循环*/
- let c=JSON.decycle(a);
- /*恢复循环*/
- JSON.retrocycle(c);
代码思路分析
JSON.decycle是负责识别循环的,主要利用new WeakMap对象,将沿途遍历的对象进行存储,主要留意的是PlainObject和数组,其他的例如数字,字符串,Date对象,Boolean对象等不会导致循环问题。
JSON.retrocycle主要负责恢复循环,需要识别$ref后的内容,主要靠的是下面的正则:
- /^\$(?:\[(?:\d+|"(?:[^\\"\u0000-\u001f]|\\(?:[\\"\/bfnrt]|u[0-9a-zA-Z]{4}))*")\])*$/
这个正则看起来比较费力,但是主要识别三种情况:
1.\d+,识别[]中出现数字,例如[1],[123]等情况
2."[^\\"\u0000-\u001f]*",识别[]中没有\,",和\u0000-\u001f范围的任意字符,例如["link"],["def"]这种情况。\u0000-\u001f里面主要是例如:\t,\r,\n等控制符号。
3.\\(?:[\\"\/bfnrt]|u[0-9a-zA-Z]{4}),识别[]中出现的部分控制符号,以及转移字符,例如["\\b"],["\\t"],["\\u1234"]等。这种情况正常不会出现,但却是可以正常被解析的情况,例如:JSON.parse('{"name":"\\b"}'),JSON.parse('{"name":"\\u1234"}')都是合法的。
识别$ref的内容后,用eval进行恢复,这个恢复还是蛮有意思的:
- let a={name:'aaa',link:{url:'ccc'}};
- (function($){
- console.log(eval($['link']));
- })(a);
eval中有一个$符号,然后给当前执行环境中设置$=a,那么$['link']就会变成a['link'],不过注意,如果是$['name']就会报错,因为$['name']的结果是'aaa',不是对象,eval执行不了。