javascript - Serialisering av objekt som inneholder syklisk objektverdi


Jeg har et objekt (parsetre) som inneholder underordnede noder som er referanser til andre noder.

Jeg vil gjerne serialisere dette objektet ved å bruke JSON.stringify(), men jeg får

TypeError: syklisk objektverdi

på grunn av konstruksjonene jeg nevnte.

Hvordan kunne jeg jobbe rundt dette? Det spiller ingen rolle for meg om disse referansene til andre noder er representert eller ikke i det serialiserte objektet.

På den annen side virker det tidkrevende å fjerne disse egenskapene fra objektet når de blir opprettet, og jeg ville ikke ønsket å gjøre endringer i parseren (narcissus).

Svar #1

Bruk den andre parameteren til stringify, erstatningsfunksjonen, for å ekskludere allerede serialiserte objekter:

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/

Som korrekt påpekt i andre kommentarer, fjerner denne koden hvert "sett" objekt, ikke bare "rekursive".

For eksempel, for:

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

resultatet vil være feil. Hvis strukturen din er slik, kan det hende du vil bruke Crockfords decycle eller denne (enklere) funksjonen som bare erstatter rekursive referanser med null:

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

Svar #2

Dette er en slags alternativ svar, men siden det mange kommer hit for er å feilsøke sine sirkulære objekter og det egentlig ikke finnes en god måte å gjøre det på uten å hente inn en masse kode, her går vi.

En funksjon som ikke er like kjent som JSON.stringify() er console.table(). Bare kall console.table(whatever);, og det vil logge variabelen i konsollen i tabellformat, noe som gjør det ganske enkelt og praktisk å gå gjennom innholdet i variabelen.

Svar #3

Her er et eksempel på en datastruktur med sirkulære referanser:

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

Når du ønsker å BEHOLDE de sykliske referansene (gjenopprette dem når du deserialiserer, i stedet for å "slette" dem), har du to valg, som jeg vil sammenligne her. Det første er Douglas Crockfords cycle.js, det andre er min siberia-pakke. Begge fungerer ved først å "de-syklisere" objektet, det vil si å konstruere et annet objekt (uten noen sykliske referanser) "som inneholder den samme informasjonen."

Mr. Crockford går først:

JSON.decycle(makeToolshed())

Som du ser, beholdes den nestede strukturen i JSON, men det er en ny ting, nemlig objekter med den spesielle $ref-egenskapen. La oss se hvordan det fungerer.

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

Dollarsymbolet står for roten. .bolt som har $ref forteller oss at .bolt er et «allerede sett» objekt, og verdien av den spesielle egenskapen (her, strengen $["nut"]["needs"]) forteller oss hvor, se første === ovenfor. På samme måte for andre $ref og den andre === ovenfor.

La oss bruke en egnet dyp likhetstest (nemlig Anders Kaseorgs deepGraphEqual-funksjon fra det aksepterte svaret på dette spørsmålet) for å se om kloning fungerer.

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

Nå, Sibir:

JSON.Siberia.forestify(makeToolshed())

Sibir prøver ikke å etterligne "klassisk" JSON, ingen nestet struktur. Objektgrafen beskrives på en "flat" måte. Hver node i objektgrafen gjøres om til et flatt tre (en enkel liste med nøkkel-verdi-par med kun heltallverdier), som er en oppføring i .forest. På indeks null finner vi roten av objektet, på høyere indekser finner vi de andre nodene i objektgrafen, og negative verdier (av noen nøkkel i noen tre i skogen) peker til atoms-arrayen, (som er typet via types-arrayen, men vi hopper over typingsdetaljene her). Alle terminalnoder er i atoms-tabellen, alle ikke-terminalnoder er i forest-tabellen, og du kan umiddelbart se hvor mange noder objektgrafen har, nemlig forest.length. La oss teste om det fungerer:

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

sammenligning

vil legge til seksjon senere.

notat

Jeg holder for øyeblikket på å refaktorisere pakken. De sentrale ideene og algoritmene forblir de samme, men den nye versjonen vil være enklere å bruke, og toppnivå-APIen vil være annerledes. Jeg vil snart arkivere siberia og presentere den refaktorerte versjonen, som jeg vil kalle objectgraph. Følg med, det vil skje denne måneden (august 2020)

ah, og ultra kort versjon for sammenligningen. For en "peker" trenger jeg like mye plass som en heltall tar, siden mine "pekere til allerede sett noder" (faktisk til alle noder, sett eller ikke sett) er bare heltall. I Mr. Crockfords versjon er mengden som trengs for å lagre en "peker" kun begrenset av størrelsen på objektgrafen. Det gjør worst case-kompleksiteten til Mr. Crockfords versjon ekstremt forferdelig. Mr. Crockford ga oss "en annen Bubblesort". Jeg tuller ikke. Det er så ille. Hvis du ikke tror meg, finnes det tester; du kan finne dem ved å starte fra readme-filen til pakken (vil også gjøre dem kompatible med benchmark.js denne måneden, august 2020)

Relaterte artikler