Сериализация объекта, содержащего циклическое значение объекта

У меня есть объект (дерево синтаксического анализа), который содержит дочерние узлы, которые являются ссылками на другие узлы.

Я хотел бы сериализовать этот объект, используя JSON.stringify(), но получаю

TypeError: циклическое значение объекта

из-за конструкций, которые я упомянул.

Как я мог обойти это? Для меня не имеет значения, представлены ли эти ссылки на другие узлы в сериализованном объекте или нет.

С другой стороны, удаление этих свойств из объекта при их создании кажется утомительным, и я бы не хотел вносить изменения в парсер (нарцисс).


person Loic Duros    schedule 21.02.2012    source источник
comment
Без кода мы не сможем вам помочь. Опубликуйте соответствующие биты вашего объекта и/или вывод JSON вместе с JS, который вы используете для его сериализации.   -  person Bojangles    schedule 21.02.2012
comment
Вы можете добавить префикс к тем свойствам, которые являются внутренними ссылками?   -  person wheresrhys    schedule 21.02.2012
comment
@Loic Было бы полезно получить здесь cycle.js Дугласа Крокфорда в качестве ответа, так как это наиболее подходящее решение для многих случаев. Вам кажется уместным опубликовать этот ответ, поскольку вы первый, кто ссылается на него (в своем комментарии ниже). Если вы не хотите публиковать это как ответ самостоятельно, я в конечном итоге сделаю это.   -  person Jeremy    schedule 24.05.2013
comment
Поздно на вечеринку, но для этого есть проект github.   -  person Preston S    schedule 30.05.2014
comment
Возможный дубликат JSON.stringify, избегайте TypeError: преобразование круговой структуры в JSON   -  person user3791372    schedule 12.02.2016
comment
Я бы хотел, чтобы JSON был умнее или проще для решения этой проблемы. Решения слишком сложны для простых (!) целей отладки imo.   -  person BluE    schedule 06.09.2019
comment
@Синий Я согласен. Я нашла отличный вариант! stackoverflow.com/a/62839421/1599699   -  person Andrew    schedule 10.07.2020


Ответы (8)


Используйте второй параметр stringify, заменитель 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/

Как правильно указано в других комментариях, этот код удаляет все «увиденные» объекты, а не только «рекурсивные».

Например, для:

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

результат будет неверным. Если ваша структура похожа на эту, вы можете использовать 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)))

person georg    schedule 21.02.2012
comment
ааа приятно! Спасибо, я собираюсь попробовать это. Я нашел решение, созданное Дугласом Крокфордом (github.com/douglascrockford/JSON -js/blob/master/cycle.js ), но, поскольку я не уверен в лицензии, которая идет с ним, простое решение, которое вы описываете, было бы идеальным! - person Loic Duros; 21.02.2012
comment
@LoicDuros Лицензия является общественным достоянием. То есть с ним можно делать все, что угодно. - person Ates Goral; 14.10.2012
comment
этот код создает зацикленные циклы, остерегайтесь его использования, так как это может привести к сбою вашего приложения. требует правильных точек с запятой и не может использоваться для объектов событий! - person Ol Sen; 22.04.2013
comment
я видел это при проверке с textmate и автоматической перезагрузкой скрипта в веб-просмотре. очень уверен, что он начал зацикливаться, а затем проверить в вашей скрипке, это сработало хорошо. поэтому я пришел к выводу, что это сделано из-за отсутствия точек с запятой, а на верхней скрипке есть обработка ошибок для себя. - person Ol Sen; 22.04.2013
comment
Это удаляет не только циклические ссылки, но и все, что появляется более одного раза. Если объект, который уже был сериализован, не является родителем нового объекта, его не следует удалять. - person Gio; 04.04.2014
comment
Хороший ответ! Я немного изменил это, превратил функцию в рекурсивную функцию, чтобы дочерние объекты клонировались так же, как клонируются родительские объекты. - person HoldOffHunger; 19.04.2018

Это своего рода альтернативный ответ, но поскольку многие люди придут сюда для отладки своих круговых объектов, и на самом деле нет хорошего способа сделать это, не втягивая кучу кода, вот и все.

Одна функция, которая не так известна, как JSON.stringify(), — это console.table(). Просто вызовите console.table(whatever);, и он запишет переменную в консоль в табличном формате, что сделает довольно простым и удобным просмотр содержимого переменной.

person Andrew    schedule 10.07.2020

Вот пример структуры данных с циклическими ссылками: toolshedCY

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

Если вы хотите СОХРАНИТЬ циклические ссылки (восстановить их при десериализации вместо уничтожения), у вас есть 2 варианта, которые я сравню здесь. Во-первых, это cycle.js Дугласа Крокфорда, во-вторых, мой < пакет href="https://github.com/mathHeadInClouds/siberia" rel="nofollow noreferrer">siberia. Оба работают, сначала дециклируя объект, т. е. создавая другой объект (без каких-либо циклических ссылок), содержащий ту же информацию.

Мистер Крокфорд идет первым:

JSON.decycle(makeToolshed())

JSON_decycleMakeToolshed

Как видите, вложенная структура JSON сохранена, но есть новинка — объекты со специальным свойством $ref. Давайте посмотрим, как это работает.

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

Знак доллара означает корень. .bolt с $ref говорит нам о том, что .bolt — это уже видимый объект, а значение этого специального свойства (здесь — строка $[nut][needs]) говорит нам, где именно, см. сначала === выше. Аналогично для второго $ref и второго === выше.

Давайте используем подходящий тест на глубокое равенство (а именно функцию 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_Siberia_forestify_makeToolshed

Сибирь не пытается имитировать классический JSON, нет вложенной структуры. Граф объекта описывается плоским образом. Каждый узел графа объектов превращается в плоское дерево (простой список пар ключ-значение с целыми значениями), который является записью в .forest. В нулевом индексе мы находим корневой объект, в более высоких индексах мы находим другие узлы графа объектов, а отрицательные значения (какого-то ключа какого-то дерева леса) указывают на массив atoms (который типизируется через массив типов, но здесь мы пропустим подробности типизации). Все конечные узлы находятся в таблице атомов, все нетерминальные узлы — в таблице леса, и сразу видно, сколько узлов у графа объекта, а именно 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 г.)

ну и ультракороткая версия для сравнения. Для указателя мне нужно столько места, сколько занимает целое число, поскольку мои указатели на уже видимые узлы (фактически, на все узлы, уже видимые или нет) являются просто целыми числами. В версии г-на Крокфорда количество, необходимое для хранения указателя, ограничено только размером графа объектов. Это делает наихудшую сложность версии мистера Крокфорда чрезвычайно ужасной. Мистер Крокфорд дал нам еще одну сортировку пузырьков. Я не шучу. Это так плохо. Если вы не верите, есть тесты, вы можете найти их, начиная с readme пакета (также в этом месяце, август 2020 г., они будут преобразованы для обеспечения совместимости с эталоном.js)

person mathheadinclouds    schedule 12.11.2019

значительно экономит и показывает, где был объект цикл.

<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__"]}]}
person Ol Sen    schedule 22.04.2013
comment
но все еще есть проблема с этим кодом, если кто-то создаст объект с obj.b=this', если кто-то знает, как предотвратить очень длинные вычисления, сделанные с неправильно заданной областью действия с this, было бы неплохо увидеть здесь - person Ol Sen; 22.04.2013
comment
Это должно быть seen.indexOf(v) != -1 - person ; 27.07.2016

Я создал 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/

person Hoff    schedule 28.03.2014

Я также создаю проект github, который может сериализовать циклический объект и восстанавливать класс, если вы сохраните его в атрибуте serializename, например String

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 и я изменить имена функций с французского на английский.

person bormat    schedule 29.03.2015
comment
Этот пример не соответствует Сути. В Gist есть ошибки. - person Ernst Ernst; 22.02.2016
comment
Хорошая идея - но один раз подготовьте :-) Если бы вы сделали его распространяемым в npm, может быть, вы бы разработали для этого даже типизацию, это, вероятно, стало бы довольно популярным. - person peterh; 21.02.2017

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.

person user3893329    schedule 08.11.2014
comment
получение RefrenceError: не удается найти переменную: _ - person amit pandya; 20.02.2020

модуль nodejs serialijse предоставляет удобный способ работы с любым типом объектов JSON, содержащих циклы или экземпляры классов javascript. .

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 и т. д.) для производительности.
person Etienne    schedule 02.05.2021