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

151

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

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

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

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

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

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

Лоик Дурос
источник
1
Мы не можем помочь вам без кода. Пожалуйста, опубликуйте соответствующие биты вашего объекта и / или вывода JSON вместе с JS, который вы используете для его сериализации.
Bojangles
1
Вы можете добавить некоторый префикс к тем свойствам, которые являются внутренними ссылками?
Wheresrhys
@Loic Было бы полезно cycle.jsполучить ответ от Дугласа Крокфорда , поскольку это наиболее подходящее решение для многих случаев. Представляется целесообразным опубликовать этот ответ, поскольку вы первый, кто на него ссылается (в своем комментарии ниже). Если вы не хотите публиковать это как ответ самостоятельно, я в конечном итоге сделаю это.
Джереми Бэнкс
1
Я бы хотел, чтобы JSON был умнее или проще для решения этой проблемы. Решения слишком хлопотны для простых (!) Целей отладки.
Блю

Ответы:

220

Используйте второй параметр stringify, в функции заменителя , чтобы исключить уже сериализованы объекты:

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)))

Georg
источник
3
аааа здорово! Спасибо, я собираюсь попробовать это. Я нашел решение, созданное Дугласом Крокфордом ( github.com/douglascrockford/JSON-js/blob/master/cycle.js ), но, поскольку я не уверен в том, что лицензия сопровождает его, простое решение, которое вы описываете, будет идеальным!
Лоик Дюрос
3
@LoicDuros Лицензия является «общественным достоянием». Это значит, что вы можете делать с ней все, что захотите.
Атес Горал
1
этот код создает циклические циклы, остерегайтесь его использования, что может привести к сбою приложения. нуждается в правильных точках с запятой и не может использоваться на объектах событий!
Ол Сен
3
Это удаляет не только циклические ссылки - оно просто удаляет все, что появляется более одного раза. Если объект, который уже был сериализован, не является «родителем» нового объекта, вы не должны удалять его
Gio
1
Хороший ответ! Я немного изменил это, превратил функцию в рекурсивную функцию, чтобы дочерние объекты клонировались так же, как клонируются родительские объекты.
HoldOffHunger
2

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

Hoff
источник
2

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

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

Андрей
источник
1

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

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

Я также создаю проект 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, и я изменил имена функций с французского на английский.

bormat
источник
Этот пример не соответствует Gist. Гист имеет ошибки.
Эрнст Эрнст
Хорошая идея - но однажды сделайте это готовым :-) Если бы вы распространяли его в npm, возможно, вы бы даже разработали для этого типизацию, это, вероятно, стало довольно популярным.
Петер - Восстановить Монику
1

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

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

Если вы хотите сохранить циклические ссылки (восстановить их при десериализации, вместо того, чтобы «обнулять» их), у вас есть 2 варианта, которые я сравню здесь. Первый - это цикл Дугласа Крокфорда. Второй - моя Сибирь. пакет. Оба работают, сначала «дециклируя» объект, т. Е. Создавая другой объект (без каких-либо циклических ссылок), «содержащий ту же информацию».

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

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

сравнение

добавлю раздел позже.

mathheadinclouds
источник
0
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.

user3893329
источник
Получение RefrenceError: Невозможно найти переменную: _
amit pandya
Пожалуйста, исправьте ваш код.
Анастасиос Мораитис