用JSON.stringify处理循环引用对象

通常,我们会用JSON.stringify把Javascript对象序列化成JSON格式,这在大多数情况下是够用的。但是,当你要转换的对象里存在循环引用时,问题就来了。

例子

var a={b:1,c:{}};  
a.d=a;  
a.c.e=a.c  
console.log(JSON.stringify(a));  

运行上面这段代码,会抛出一个异常,因为这里存在循环引用:

TypeError: Converting circular structure to JSON  

关于JSON.stringify1

JSON对象是Javascript语言级别的内置对象,在任何Javascript环境中,都能方便的使用它。

关于它的更多细节,可以在这里查看。

我总结了几个关键点:

  • 非数组对象的顺序是不确定的
  • undefined、任意的函数以及 symbol 值,会被忽略(不在数组中)或转换成null(数组中)
  • 如果对象存在toJSON函数,会对toJSON的返回值进行序列化
  • 如果存在循环引用,会抛出异常
  • 可以传入第二个参数,在属性值被序列化之前改变它

使用util.inspect2

util是Node.js的标准库,它提供了inspect函数。它的作用和JSON.stringify很像,可以把对象转化为字符串,而且还有以下特点:

  • 不会忽略函数,显示[Function],也不会忽略undefined和symbol
  • 对于循环引用,会显示[Circular]
  • 可以设置对象深度,超过的不再继续向内显示,而是显示[Object]
  • 如果对象存在inspect函数,则会对其结果进行转换
var util=require('util');  
var a={b:1};  
a.c=a;  
console.log(util.inspect(a));  

它会输出以下内容:

{ b: 1, c: [Circular] }

哈,看来这个函数可以解决我们的问题了。

但是,要注意的是
它输出的内容不是JSON格式!
它输出的内容不是JSON格式!
它输出的内容不是JSON格式!

如果你只是为了打印,那么util.inspect也够了。但是如果你有序列化/反序列化需要,你是无法通过JSON.parse将其转化为Javascript对象的。如果你真的有这种需求,可以对其结果再做一些处理或者专门写一个针对其输出格式的反序列化工具。

改进JSON.stringify

很有可能你需要输出的是JSON格式,或者在你的代码中已经大量的使用了JSON.stringify/JSON.parse,那么util.inspect就帮不了你了。

下面我们利用JSON.stringify的第二个参数来改进它,在循环引用的地方显示[Circular xxx],xxx为引用对象的key,如果为顶层对象则为root。

代码如下(参考了StackOverflow上的几个问答3 4):

var handleCircular = function() {  
    var cache = [];
    var keyCache = []
    return function(key, value) {
        if (typeof value === 'object' && value !== null) {
            var index = cache.indexOf(value);
            if (index !== -1) {
                return '[Circular ' + keyCache[index] + ']';
            }
            cache.push(value);
            keyCache.push(key || 'root');
        }
        return value;
    }
}

var tmp = JSON.stringify;  
JSON.stringify = function(value, replacer, space) {  
    replacer = replacer || handleCircular();
    return tmp(value, replacer, space);
}

注意,上面的代码会覆盖JSON.stringify函数,如果你不想覆盖,请自行修改。

再运行文章开头的代码,会得到以下输出:

{"b":1,"c":{"e":"[Circular c]"},"d":"[Circular root]"}

对于这个输出,是可以使用JSON.parse来把它转换成对象的,当然,原本循环引用的地方会变成字符串。

更多

那么,能不能用JSON.parse把循环引用的对象也复原呢,笔者认为是可以,JSON.parse也提供了第二个参数5用来在返回结果前对其进行修改。读者如果感兴趣可以自行尝试。

参考资料