Серіалізуючий об'єкт, що містить циклічне значення об'єкта


151

У мене є об'єкт (дерево розбору), який містить дочірні вузли, посилання на інші вузли.

Я хотів би серіалізувати цей об’єкт, використовуючи JSON.stringify(), але я отримую

TypeError: значення циклічного об'єкта

через згадані нами конструкції.

Як я міг обійти це? Мені неважливо, представлені ці посилання на інші вузли в серіалізованому об'єкті чи ні.

З іншого боку, видалення цих властивостей з об'єкта, коли вони створюються, здається втомливим, і я не хотів би вносити зміни в аналізатор (нарцис).


1
Ми не можемо вам допомогти без коду. Будь ласка, опублікуйте відповідні біти вашого об'єкта та / або JSON виводу разом із JS, який ви використовуєте для його серіалізації.
Bojangles

1
чи можете ви додати якийсь префікс до тих властивостей, які є внутрішніми посиланнями?
wheresrhys

@Loic Тут було б корисно cycle.jsвідповісти Дугласа Крокфорда , оскільки це найбільш підходяще рішення для багатьох випадків. Вам здається доречним опублікувати цю відповідь, оскільки ви перший посилаєтесь на неї (у коментарі нижче). Якщо ви не захочете опублікувати це як відповідь, я зрештою це зроблю.
Джеремі Бенкс


1
Я б хотів, щоб JSON був розумнішим чи простішим способом вирішення цього питання. Рішення занадто клопітні для простих (!) Цілей налагодження imo.
BluE

Відповіді:


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];

результат буде неправильним. Якщо ваша структура подібна до такої, ви, можливо, захочете використовувати цикл Крокфорда або цю (більш просту) функцію, яка просто замінює рекурсивні посилання нулями:

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


3
ааа приємно! Спасибі, я спробую це. Я знайшов рішення, створене Дугласом Крокфордом ( github.com/douglascrockford/JSON-js/blob/master/cycle.js ), але, оскільки я не впевнений у ліцензії, яка йде разом із цим, просте рішення, яке ви описуєте, було б ідеальним!
Loic Duros

3
@LoicDuros Ліцензія є "загальнодоступним доступом". Це означає, що ви можете робити все, що завгодно.
Атес Горал

1
цей код створює велосипедні петлі, будьте обережні, використовуйте дуже потенційні збої у вашій програмі. потрібні правильні крапки з комою та не використовуються на об'єктах події!
Ol Sen

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/


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


Цей приклад не відповідає суті. У Гісті є помилки.
Ернст Ернст

Хороша ідея - але колись зробіть її готовою :-) Якщо ви зробите це для розповсюдження в npm, можливо, ви б розробили для цього навіть типізацію, вона, напевно, стала досить популярною.
петерх

1

Ось приклад структури даних з циклічними посиланнями: інструментальнийCY

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

Коли ви хочете зберегти циклічні посилання (відновити їх, коли ви дезаріалізуєте, замість того, щоб "нукірувати" їх), у вас є два варіанти, які я порівняю тут. По- перше це Дугласа Крокфорд в cycle.js , другий мій сибір пакет. Обидва працюють, спочатку "дециркулюючи" об'єкт, тобто будуючи інший об'єкт (без будь-яких циклічних посилань) ", що містить ту саму інформацію".

Містер Крокфорд виходить першим:

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це «вже бачив» об'єкт, і значення цього особливого властивості (тут, рядок $ [ «горіх»] [ «потреби»]) говорить нам , де, см перший ===вище. Так само і для другого, $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

порівняння

буде додано розділ пізніше.


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.


отримання RefrenceError: Неможливо знайти змінну: _
amit pandya

Виправте свій код.
Анастасіос Мораїт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.