Serializacja obiektu, który zawiera cykliczną wartość obiektu

151

Mam obiekt (drzewo parsowania), który zawiera węzły potomne, które są odniesieniami do innych węzłów.

Chciałbym serializować ten obiekt, używając JSON.stringify(), ale otrzymuję

TypeError: cykliczna wartość obiektu

ze względu na konstrukcje, o których wspomniałem.

Jak mogłem to obejść? Nie ma dla mnie znaczenia, czy te odwołania do innych węzłów są reprezentowane, czy nie w serializowanym obiekcie.

Z drugiej strony usuwanie tych właściwości z obiektu podczas ich tworzenia wydaje się żmudne i nie chciałbym wprowadzać zmian w parserze (narcyz).

Loic Duros
źródło
1
Nie możemy Ci pomóc bez kodu. Prześlij odpowiednie bity obiektu i / lub dane wyjściowe JSON wraz z kodem JS, którego używasz do serializacji.
Bojangles,
1
czy możesz dodać przedrostek do tych właściwości, które są odwołaniami wewnętrznymi?
wheresrhys
@Loic Cenne byłoby posiadanie odpowiedzi Douglasa Crockforda cycle.js, ponieważ jest to najbardziej odpowiednie rozwiązanie w wielu przypadkach. Wydaje się właściwe, abyś opublikował tę odpowiedź, ponieważ jesteś pierwszą osobą, która się do niej odwołuje (w komentarzu poniżej). Jeśli nie masz ochoty publikować tego jako odpowiedzi, w końcu to zrobię.
Jeremy Banks
1
Chciałbym, żeby JSON był mądrzejszy lub łatwiejszy do rozwiązania tego problemu. Rozwiązania te są zbyt kłopotliwe dla prostych (!) Celów debugowania imo.
BluE

Odpowiedzi:

220

Użyj drugi parametr stringify, na funkcję zamiennika , aby wykluczyć już szeregowane obiekty:

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/

Jak słusznie wskazano w innych komentarzach, kod ten usuwa każdy „widziany” obiekt, nie tylko „rekurencyjny”.

Na przykład dla:

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

wynik będzie nieprawidłowy. Jeśli twoja struktura jest taka, możesz chcieć użyć decyklu Crockforda lub tej (prostszej) funkcji, która po prostu zastępuje odwołania rekurencyjne zerami:

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
źródło
3
aaach fajnie! Dzięki, spróbuję tego. Znalazłem rozwiązanie stworzone przez Douglasa Crockforda ( github.com/douglascrockford/JSON-js/blob/master/cycle.js ), ale ponieważ nie jestem pewien, jaka licencja się z tym wiąże, proste rozwiązanie, które opisujesz , byłoby idealne!
Loic Duros,
3
@LoicDuros Licencja jest „domena publiczna”. Oznacza to, że możesz z nim zrobić wszystko, co chcesz.
Ates Goral
1
ten kod tworzy pętle rowerowe, uważaj na używanie, bardzo potencjalne awarie aplikacji. wymaga poprawnych średników i nie można go używać na obiektach zdarzeń!
Ol Sen
3
To usuwa więcej niż tylko cykliczne odniesienia - po prostu usuwa wszystko, co pojawia się więcej niż raz. O ile obiekt, który został już zserializowany, nie jest „rodzicem” nowego obiektu, nie należy go usuwać
Gio
1
Dobra odpowiedź! Zmodyfikowałem to trochę, zmieniłem funkcję na funkcję rekurencyjną, aby obiekty potomne były klonowane w taki sam sposób, jak klonowane są obiekty nadrzędne.
HoldOffHunger
2

Stworzyłem GitHub Gist, który jest w stanie wykryć struktury cykliczne, a także je odkodować i zakodować: https://gist.github.com/Hoff97/9842228

Aby przekształcić po prostu użyj JSONE.stringify / JSONE.parse. Ponadto dekoduje i koduje funkcje. Jeśli chcesz to wyłączyć, po prostu usuń linie 32-48 i 61-85.

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

Przykładowe skrzypce znajdziesz tutaj:

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

Hoff
źródło
2

To jest rodzaj alternatywnej odpowiedzi, ale ponieważ wiele osób przyjdzie tutaj, to debugowanie ich okrągłych obiektów, a nie ma naprawdę świetnego sposobu na zrobienie tego bez wciągania dużej ilości kodu, oto jest.

Jedna funkcja, która nie jest tak znana, jak JSON.stringify()jest console.table(). Po prostu wywołaj console.table(whatever);, a zapisze zmienną w konsoli w formacie tabelarycznym, dzięki czemu przeglądanie zawartości zmiennej będzie dość łatwe i wygodne.

Andrzej
źródło
1

znacznie oszczędza i pokazuje, gdzie był obiekt cyklu .

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

produkuje

jsonify = {"g":{"d":[2,5],"j":2},"e":10,"someloopshere":[{"d":[2,5],"j":2},"__cycle__",{"a":[10,"__cycle__"]}]}
Ol Sen
źródło
ale nadal jest problem z tym kodem, jeśli ktoś zbudowałby obiekt, obj.b=this'gdyby ktoś wiedział, jak zapobiec bardzo długim calcom wykonanym z niewłaściwego podanego zakresu, thisbyłoby miło zobaczyć tutaj
Ol Sen
2
To powinno byćseen.indexOf(v) != -1
1

Tworzę też projekt na githubie, który może serializować cykliczny obiekt i przywrócić klasę, jeśli zapiszesz ją w atrybucie serializename, takim jak 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

Edycja: Przekształciłem mój skrypt dla NPM https://github.com/bormat/borto_circular_serialize i zmieniłem nazwy funkcji z francuskiego na angielski.

bormat
źródło
Ten przykład nie pasuje do Gist. Gist zawiera błędy.
Ernst Ernst
Fajny pomysł - ale raz go przygotuj :-) Gdybyś rozprowadził go w npm, być może stworzyłbyś nawet dla tego typy, prawdopodobnie stałby się całkiem popularny.
peterh - Przywróć Monikę
1

Oto przykład struktury danych z cyklicznymi odwołaniami: toolshedCY

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

Kiedy chcesz ZACHOWAĆ cykliczne odniesienia (przywróć je podczas deserializacji, zamiast „nukingować” je), masz 2 możliwości, które porównam tutaj. Pierwszy to cycle.js Douglasa Crockforda , drugi to mój pakiet syberii . Obie działają najpierw „decyklując” obiekt, tj. Konstruując inny obiekt (bez żadnych cyklicznych odniesień) „zawierający te same informacje”.

Pan Crockford idzie pierwszy:

JSON.decycle(makeToolshed())

JSON_decycleMakeToolshed

Jak widać, zagnieżdżona struktura JSON jest zachowywana, ale jest nowa rzecz, czyli obiekty ze specjalną $refwłaściwością. Zobaczmy, jak to działa.

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

Znak dolara oznacza korzeń. .boltmając $refmówi nam, że .boltjest to obiekt „już widziany”, a wartość tej specjalnej właściwości (tutaj ciąg $ [„nut”] [„need”]) mówi nam gdzie, patrz ===wyżej. Podobnie dla drugiego $refi drugiego ===powyżej.

Użyjmy odpowiedniego głębokiego testu równości (mianowicie funkcji Andersa Kaseorga deepGraphEqualz zaakceptowanej odpowiedzi na to pytanie ), aby sprawdzić, czy klonowanie działa.

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

Teraz Syberia:

JSON.Siberia.forestify(makeToolshed())

JSON_Siberia_forestify_makeToolshed

Syberia nie próbuje naśladować „klasycznego” JSON, bez zagnieżdżonej struktury. Graf obiektowy jest opisany w sposób „płaski”. Każdy węzeł wykresu obiektu jest przekształcany w płaskie drzewo (zwykła lista par kluczy-wartość z wartościami całkowitymi), która jest wpisem w .forest.indeksie zerowym, znajdujemy obiekt główny, przy wyższych indeksach znajdujemy inne węzły wykres obiektu i wartości ujemne (jakiegoś klucza jakiegoś drzewa w lesie) wskazują na atomstablicę (która jest wpisywana poprzez tablicę types, ale pominiemy tutaj szczegóły wpisywania). Wszystkie węzły końcowe znajdują się w tabeli atomów, wszystkie węzły nieterminalne znajdują się w tabeli lasu i od razu można zobaczyć, ile węzłów ma graf obiektu, a mianowicie forest.length. Sprawdźmy, czy to działa:

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

porównanie

doda sekcję później.

mathheadinclouds
źródło
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
    }
  );
}

Brakowało warunku wstępnego, w przeciwnym razie wartości całkowite w obiektach tablic zostaną obcięte, tj. [[08.11.2014 12:30:13, 1095]] 1095 zostanie zmniejszone do 095.

user3893329
źródło
uzyskiwanie RefrenceError: Can't find variable: _
amit pandya
Popraw kod.
Anastasios Moraitis