【问题标题】:Serializing object that contains cyclic object value序列化包含循环对象值的对象
【发布时间】:2012-03-12 00:51:00
【问题描述】:

我有一个对象(分析树),其中包含对其他节点的引用的子节点。

我想序列化这个对象,使用JSON.stringify(),但我明白了

TypeError: 循环对象值

因为我提到的构造。

我该如何解决这个问题?这些对其他节点的引用是否在序列化对象中表示对我来说并不重要。

另一方面,在创建对象时从对象中删除这些属性似乎很乏味,我不想对解析器进行更改(水仙)。

【问题讨论】:

  • 如果没有一些代码,我们将无法帮助您。请将您的对象和/或 JSON 输出的相关位与您用于序列化它的 JS 一起发布。
  • 你能在那些属于内部引用的属性上添加一些前缀吗?
  • @Loic 在这里找到 Douglas Crockford 的 cycle.js 作为答案会很有价值,因为它是很多情况下最合适的解决方案。您似乎适合发布该答案,因为您是第一个引用它的人(在下面的评论中)。如果您不想自己发布它作为答案,我最终会这样做。
  • 我希望 JSON 会更聪明,或者更容易解决这个问题。解决方案对于简单(!)调试目的太麻烦了imo。

标签: javascript json jsonserializer stringify


【解决方案1】:

使用stringify的第二个参数replacer function,排除已经序列化的对象:

var seen = [];

JSON.stringify(obj, function(key, val) {
   if (val != null && typeof val == "object") {
        if (seen.indexOf(val) >= 0) {
            return;
        }
        seen.push(val);
    }
    return val;
});

http://jsfiddle.net/mH6cJ/38/

正如在其他 cmets 中正确指出的那样,此代码删除了每个“可见”对象,而不仅仅是“递归”对象。

例如,对于:

a = {x:1};
obj = [a, a];

结果将不正确。如果你的结构是这样的,你可能想使用 Crockford 的 decycle 或这个(更简单的)函数,它只是用空值替换递归引用:

function decycle(obj, stack = []) {
    if (!obj || typeof obj !== 'object')
        return obj;
    
    if (stack.includes(obj))
        return null;

    let s = stack.concat([obj]);

    return Array.isArray(obj)
        ? obj.map(x => decycle(x, s))
        : Object.fromEntries(
            Object.entries(obj)
                .map(([k, v]) => [k, decycle(v, s)]));
}

//

let a = {b: [1, 2, 3]}
a.b.push(a);

console.log(JSON.stringify(decycle(a)))

【讨论】:

  • 啊啊好极了!谢谢,我要试试这个。我找到了由 Douglas Crockford (github.com/douglascrockford/JSON-js/blob/master/cycle.js) 创建的解决方案,但由于我不确定随附的许可证,因此您描述的简单解决方案将是完美的!
  • @LoicDuros 许可证是“公共领域”。意思是,你可以用它做任何你想做的事情。
  • 这段代码会产生循环,小心使用,很可能会导致您的应用崩溃。需要正确的分号并且不能用于事件对象!
  • 这不仅删除了循环引用 - 它只是删除了多次出现的任何内容。除非已经序列化的对象是新对象的“父对象”,否则不应删除它
  • 好答案!我稍作修改,将函数更改为递归函数,这样子对象就会像克隆父对象一样被克隆。
【解决方案2】:

这是一种替代答案,但是由于很多人会来这里是为了调试他们的圆形对象,并且如果不引入一堆代码并没有真正的好方法来做到这一点,那就去吧。

一个不像JSON.stringify() 那样知名的功能是console.table()。只需调用console.table(whatever);,它就会以表格的形式将变量记录在控制台中,使得阅读变量的内容变得相当简单方便。

【讨论】:

    【解决方案3】:

    下面是一个带有循环引用的数据结构示例:

    function makeToolshed(){
        var nut = {name: 'nut'}, bolt = {name: 'bolt'};
        nut.needs = bolt; bolt.needs = nut;
        return { nut: nut, bolt: bolt };
    }
    

    当您希望 KEEP 循环引用(在反序列化时恢复它们,而不是“破坏”它们),您有 2 个选择,我将比较这里。首先是 Douglas Crockford 的 cycle.js,其次是我的 siberia 包。两者都首先“回收”对象,即构造另一个“包含相同信息”的对象(没有任何循环引用)。

    先生。康乐福先行:

    JSON.decycle(makeToolshed())
    

    如您所见,JSON 的嵌套结构被保留了,但有一个新的东西,即具有特殊$ref 属性的对象。让我们看看它是如何工作的。

    root = makeToolshed();
    [root.bolt === root.nut.needs, root.nut.needs.needs === root.nut]; // retutrns [true,true]
    

    美元符号代表根。 .bolt 拥有$ref 告诉我们.bolt 是一个“已经见过”的对象,并且那个特殊属性的值(这里是字符串 $["nut"]["needs"])告诉我们在哪里,见第一个=== 上面。同样适用于第二个$ref 和第二个===

    让我们使用合适的深度相等测试(即从接受的答案到this question 的 Anders Kaseorg 的 deepGraphEqual 函数)来看看克隆是否有效。

    root = makeToolshed();
    clone = JSON.retrocycle(JSON.decycle(root));
    deepGraphEqual(root, clone) // true
    serialized = JSON.stringify(JSON.decycle(root));
    clone2 = JSON.retrocycle(JSON.parse(serialized));
    deepGraphEqual(root, clone2); // true
    

    现在,西伯利亚:

    JSON.Siberia.forestify(makeToolshed())
    

    西伯利亚不会尝试模仿“经典”JSON,没有嵌套结构。对象图以“平面”方式描述。 对象图的每个节点都变成了一个扁平树(纯键值对列表,只有整数值),它是.forest. 中的一个条目,在索引零处,我们找到根对象,在更高的索引处,我们找到对象图的其他节点和(森林中某棵树的某个键的某个键的)负值指向atoms 数组(通过 types 数组输入,但我们将在此处跳过输入细节)。所有终端节点都在原子表中,所有非终端节点都在森林表中,您可以立即看到对象图有多少个节点,即forest.length。让我们测试它是否有效:

    root = makeToolshed();
    clone = JSON.Siberia.unforestify(JSON.Siberia.forestify(root));
    deepGraphEqual(root, clone); // true
    serialized = JSON.Siberia.stringify(JSON.Siberia.forestify(root));
    clone2 = JSON.Siberia.unforestify(JSON.Siberia.unstringify(serialized));
    deepGraphEqual(root, clone2); // true
    

    比较

    稍后会添加部分。

    注意

    我目前正在重构包。中心思想和算法保持不变,但新版本将更易于使用,顶级 API 将有所不同。我将很快归档 siberia 并展示重构的版本,我将其称为 objectgraph。敬请期待,它将在本月(2020 年 8 月)发生

    啊,和超短版比较。对于“指针”,我需要与整数一样多的空间,因为我的“指向已经看到的节点的指针”(事实上,指向所有节点,无论是否已经看到)整数。在 Crockford 先生的版本中,存储“指针”所需的数量仅受对象图大小的限制。这使得 Crockford 先生的版本的最坏情况复杂性极其可怕。 Crockford 先生给了我们“另一个 Bubblesort”。我不是在开玩笑。真是太糟糕了。如果您不相信,这里有测试,您可以从包的自述文件开始找到它们(也将在本月,2020 年 8 月将它们转换为符合 benchmark.js)

    【讨论】:

    • 我通过npm i cycle 安装了cycle.js,但我得到了一个TypeError:JSON.decycle is not a function。我需要导入decycle方法吗?如果是这样,我该如何导入它?
    【解决方案4】:

    非常节省,它显示了 cycle 对象的位置。

    <script>
    var jsonify=function(o){
        var seen=[];
        var jso=JSON.stringify(o, function(k,v){
            if (typeof v =='object') {
                if ( !seen.indexOf(v) ) { return '__cycle__'; }
                seen.push(v);
            } return v;
        });
        return jso;
    };
    var obj={
        g:{
            d:[2,5],
            j:2
        },
        e:10
    };
    obj.someloopshere = [
        obj.g,
        obj,
        { a: [ obj.e, obj ] }
    ];
    console.log('jsonify=',jsonify(obj));
    </script>
    

    生产

    jsonify = {"g":{"d":[2,5],"j":2},"e":10,"someloopshere":[{"d":[2,5],"j":2},"__cycle__",{"a":[10,"__cycle__"]}]}
    

    【讨论】:

    • 但是如果有人知道如何防止使用this 使用错误的给定范围进行很长的计算,那么如果有人使用obj.b=this' 构建一个对象,这段代码仍然存在问题看这里
    • 这应该是seen.indexOf(v) != -1
    【解决方案5】:

    我创建了一个 GitHub Gist,它能够检测循环结构并对其进行解码和编码:https://gist.github.com/Hoff97/9842228

    只需使用 JSONE.stringify/JSONE.parse 进行转换。 它还对函数进行解译和编码。如果您想禁用此功能,只需删除第 32-48 和 61-85 行即可。

    var strg = JSONE.stringify(cyclicObject);
    var cycObject = JSONE.parse(strg);
    

    你可以在这里找到一个示例小提琴:

    http://jsfiddle.net/hoff97/7UYd4/

    【讨论】:

      【解决方案6】:

      我也创建了一个可以序列化循环对象并恢复类的github项目,如果你将它保存在像字符串一样的serializename属性中

      var d={}
      var a = {b:25,c:6,enfant:d};
      d.papa=a;
      var b = serializeObjet(a);
      assert.equal(  b, "{0:{b:25,c:6,enfant:'tab[1]'},1:{papa:'tab[0]'}}" );
      var retCaseDep = parseChaine(b)
      assert.equal(  retCaseDep.b, 25 );
      assert.equal(  retCaseDep.enfant.papa, retCaseDep );
      

      https://github.com/bormat/serializeStringifyParseCyclicObject

      编辑: 我已将我的脚本转换为 NPM https://github.com/bormat/borto_circular_serialize,并将函数名称从法语更改为英语。

      【讨论】:

      • 这个例子不符合要点。要点有错误。
      • 好主意 - 但是一旦准备好 :-) 如果你让它在 npm 中分发,也许你会为此开发甚至类型,它可能会变得非常流行。
      【解决方案7】:
      function stringifyObject ( obj ) {
        if ( _.isArray( obj ) || !_.isObject( obj ) ) {
          return obj.toString()
        }
        var seen = [];
        return JSON.stringify(
          obj,
          function( key, val ) {
            if (val != null && typeof val == "object") {
              if ( seen.indexOf( val ) >= 0 )
                return
                seen.push( val )
                }
            return val
          }
        );
      }
      

      缺少前提条件,否则数组对象中的整数值将被截断,即 [[ 08.11.2014 12:30:13, 1095 ]] 1095 会减少到 095。

      【讨论】:

      • 得到 RefrenceError: 找不到变量:_
      【解决方案8】:

      nodejs 模块serialijse 提供了一种很好的方式来处理包含循环或javascript 类实例的任何类型的JSON 对象。

      const { serialize, deserialize } = require("serialijse");
      
      
          var Mary = { name: "Mary", friends: [] };
          var Bob = { name: "Bob", friends: [] };
      
          Mary.friends.push(Bob);
          Bob.friends.push(Mary);
      
          var group = [ Mary, Bob];
          console.log(group);
      
          // testing serialization using  JSON.stringify/JSON.parse
          try {
              var jstr = JSON.stringify(group);
              var jo = JSON.parse(jstr);
              console.log(jo);
      
          } catch (err) {
              console.log(" JSON has failed to manage object with cyclic deps");
              console.log("  and has generated the following error message", err.message);
          }
      
          // now testing serialization using serialijse  serialize/deserialize
          var str = serialize(group);
          var so = deserialize(str);
          console.log(" However Serialijse knows to manage object with cyclic deps !");
          console.log(so);
          assert(so[0].friends[0] == so[1]); // Mary's friend is Bob
      
      

      这个序列化器支持

      • 对象定义中的循环
      • 类实例的重构
      • 支持类型化数组、映射和集合
      • 能够过滤属性以在序列化过程中跳过。
      • 类型化数组(Float32Array 等...)的二进制编码以提高性能。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-01-01
        • 2014-07-10
        • 1970-01-01
        • 2011-12-02
        • 1970-01-01
        • 2018-03-20
        相关资源
        最近更新 更多