¿Cómo puedo imprimir una estructura circular en un formato similar a JSON?


680

Tengo un gran objeto que quiero convertir a JSON y enviar. Sin embargo tiene estructura circular. Quiero tirar cualquier referencia circular que exista y enviar lo que pueda ser encadenado. ¿Cómo puedo hacer eso?

Gracias.

var obj = {
  a: "foo",
  b: obj
}

Quiero encadenar obj en:

{"a":"foo"}

55
¿Podría publicar un objeto de muestra con una referencia circular que le gustaría analizar?
TWickz

3
algo como esto ?
Alvin Wong

1
posible duplicado del objeto
Oleg V. Volkov

2
Tarde a la fiesta, pero hay un proyecto Github para manejar esto.
Preston S

pregunta estrechamente relacionada: stackoverflow.com/questions/23117470/…
mathheadinclouds

Respuestas:


605

Usar JSON.stringifycon un sustituto personalizado. Por ejemplo:

// Demo: Circular reference
var circ = {};
circ.circ = circ;

// Note: cache should not be re-used by repeated calls to JSON.stringify.
var cache = [];
JSON.stringify(circ, (key, value) => {
  if (typeof value === 'object' && value !== null) {
    // Duplicate reference found, discard key
    if (cache.includes(value)) return;

    // Store value in our collection
    cache.push(value);
  }
  return value;
});
cache = null; // Enable garbage collection

El sustituto en este ejemplo no es 100% correcto (dependiendo de su definición de "duplicado"). En el siguiente caso, se descarta un valor:

var a = {b:1}
var o = {};
o.one = a;
o.two = a;
// one and two point to the same object, but two is discarded:
JSON.stringify(o, ...);

Pero el concepto se mantiene: utilice un reemplazador personalizado y realice un seguimiento de los valores de los objetos analizados.

Como una función de utilidad escrita en es6:

// safely handles circular references
JSON.safeStringify = (obj, indent = 2) => {
  let cache = [];
  const retVal = JSON.stringify(
    obj,
    (key, value) =>
      typeof value === "object" && value !== null
        ? cache.includes(value)
          ? undefined // Duplicate reference found, discard key
          : cache.push(value) && value // Store value in our collection
        : value,
    indent
  );
  cache = null;
  return retVal;
};

// Example:
console.log('options', JSON.safeStringify(options))

1
@Harry ¿Cuál es el error? Con gusto arreglaré la respuesta, si hay alguna imprecisión en ella.
Rob W

1
@CruzDiablo Serializing DOM generalmente no tiene sentido. Sin embargo, si puede pensar en un método de serialización significativo para sus propósitos, entonces podría intentar agregar un serializado personalizado a objetos DOM: Node.prototype.toJSON = function() { return 'whatever you think that is right'; };(si desea algo más genérico / específico, simplemente intente cualquier cosa en el árbol del prototipo: HTMLDivElement implementa HTMLElement implementa Element implementa Node implementa EventTarget; nota: esto puede depender del navegador, el árbol anterior es verdadero para Chrome)
Rob W

77
esto está mal porque omitirá la segunda aparición de objetos que están contenidos dos veces, incluso si no está en una estructura realmente cíclica. var a={id:1}; JSON.stringify([a,a]);
user2451227

3
@ user2451227 "El sustituto en este ejemplo no es 100% correcto (dependiendo de su definición de" duplicado "). Pero el concepto es válido: utilice un sustituto personalizado y realice un seguimiento de los valores de los objetos analizados".
Rob W

44
La preocupación de GC aquí es posiblemente redundante. Si esto se ejecuta como un solo script, el script termina inmediatamente. Si esto está encapsulado dentro de una función para la implementación, cacheserá inalcanzable developer.mozilla.org/en-US/docs/Web/JavaScript/…
Trindaz

704

En Node.js, puede usar util.inspect (objeto) . Reemplaza automáticamente los enlaces circulares con "[Circular]".


Aunque está integrado (no se requiere instalación) , debe importarlo

import * as util from 'util' // has no default export
import { inspect } from 'util' // or directly
// or 
var util = require('util')
Para usarlo, simplemente llame
console.log(util.inspect(myObject))

También tenga en cuenta que puede pasar el objeto de opciones para inspeccionar (vea el enlace de arriba)

inspect(myObject[, options: {showHidden, depth, colors, showProxy, ...moreOptions}])



Por favor, lea y felicite a los comentaristas a continuación ...


134
util es un módulo incorporado, no es necesario que lo instales.
Mitar

10
console.log (util.inspect (obj))
starsinmypockets

19
@Mitar está integrado, pero aún tiene que cargar el módulovar util = require('util');
bodecker

14
No seas un tonto como yo, es solo obj_str = util.inspect(thing) , NO <s> garbage_str = JSON.stringify(util.inspect(thing))</s>
ThorSummoner

77
Esto es mucho mejor que perder el tiempo con tipos de verificación. ¿Por qué no puede stringify simplemente trabajar así? Si sabe que hay una referencia circular, ¿por qué no se le puede decir que la ignore?
Chris Peacock

141

Me pregunto por qué nadie publicó la solución adecuada desde la página de MDN todavía ...

const getCircularReplacer = () => {
  const seen = new WeakSet();
  return (key, value) => {
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) {
        return;
      }
      seen.add(value);
    }
    return value;
  };
};

JSON.stringify(circularReference, getCircularReplacer());

Los valores vistos deben almacenarse en un conjunto , no en una matriz (se llama al reemplazador en cada elemento ) y no hay necesidad de probar JSON.stringify cada elemento de la cadena que conduce a una referencia circular.

Como en la respuesta aceptada, esta solución elimina todos los valores repetidos , no solo los circulares. Pero al menos no tiene una complejidad exponencial.


Aseado, pero esto es solo ES2015. No es compatible con IE.
Martin Capodici

43
Yoda dice: "Si todavía es compatible con IE one, entonces use un transpilador que debería".
Tren de España el

1
replacer = () => { const seen = new WeakSet(); return (key, value) => { if (typeof value === "object" && value !== null) { if (seen.has(value)) { return; } seen.add(value); } return value; }; } () => { const seen = new WeakSet(); return (key, value) => { if (typeof value === "object" && value !== null) { if (seen.has(value)) { return; } seen.add(value); … JSON.stringify({a:1, b: '2'}, replacer)regresa undefineden cromo
roberto tomás

1
Funciona en React + Typecript. gracias
user3417479

76

solo haz

npm i --save circular-json

entonces en tu archivo js

const CircularJSON = require('circular-json');
...
const json = CircularJSON.stringify(obj);

https://github.com/WebReflection/circular-json

NOTA: No tengo nada que ver con este paquete. Pero lo uso para esto.

Actualización 2020

Tenga en cuenta que CircularJSON solo está en mantenimiento y Flatted es su sucesor.


¡Muchas gracias! Gran biblioteca, ahorró toneladas de tiempo. Super pequeño (solo 1.4KB minificado).
Brian Haak

16
Creo que podría requerir más justificación para usar un módulo que "simplemente hacer". Y no es genial sobrescribir JSONpor principio.
Edwin

Necesitaba copiar un objeto para usarlo en la prueba de código auxiliar. Esta respuesta fue perfecta. Copié el objeto y luego eliminé la anulación. ¡¡Gracias!!
Chris Sharp

1
Según el autor, este paquete ha quedado en desuso. CircularJSON solo está en mantenimiento, Flatted es su sucesor. Enlace: github.com/WebReflection/flatted#flatted
Robert Molina

3
Cuidado, el paquete 'flatted' (y circular-json?) No replica la funcionalidad JSON.stringify (). Crea su propio formato no JSON. (p. ej., Flatted.stringify({blah: 1})resultados [{"blah":1}]) Veo que alguien trató de plantear un problema al respecto, y el autor los reprendió y bloqueó el tema a los comentarios.
jameslol

48

Realmente me gustó la solución de Trindaz, más detallada, sin embargo, tenía algunos errores. También los arreglé para quien quiera.

Además, agregué un límite de longitud en mis objetos de caché.

Si el objeto que estoy imprimiendo es realmente grande, quiero decir infinitamente grande, quiero limitar mi algoritmo.

JSON.stringifyOnce = function(obj, replacer, indent){
    var printedObjects = [];
    var printedObjectKeys = [];

    function printOnceReplacer(key, value){
        if ( printedObjects.length > 2000){ // browsers will not print more than 20K, I don't see the point to allow 2K.. algorithm will not be fast anyway if we have too many objects
        return 'object too long';
        }
        var printedObjIndex = false;
        printedObjects.forEach(function(obj, index){
            if(obj===value){
                printedObjIndex = index;
            }
        });

        if ( key == ''){ //root element
             printedObjects.push(obj);
            printedObjectKeys.push("root");
             return value;
        }

        else if(printedObjIndex+"" != "false" && typeof(value)=="object"){
            if ( printedObjectKeys[printedObjIndex] == "root"){
                return "(pointer to root)";
            }else{
                return "(see " + ((!!value && !!value.constructor) ? value.constructor.name.toLowerCase()  : typeof(value)) + " with key " + printedObjectKeys[printedObjIndex] + ")";
            }
        }else{

            var qualifiedKey = key || "(empty key)";
            printedObjects.push(value);
            printedObjectKeys.push(qualifiedKey);
            if(replacer){
                return replacer(key, value);
            }else{
                return value;
            }
        }
    }
    return JSON.stringify(obj, printOnceReplacer, indent);
};

Le falta una comprobación nula en esta línea: return "(vea" + (!! value.constructor? Value.constructor.name.toLowerCase (): typeof (value)) + "con clave" + printedObjectKeys [printedObjIndex] + ")";
Isak

Con mucho gusto lo agregaré. solo avíseme qué es anulable ya que hasta ahora tuve problemas.
Guy Mograbi

2
// los navegadores no imprimirán más de 20K, pero pones el límite como 2k. Tal vez cambiar para el futuro?
Pochen

38

La respuesta de @ RobW es correcta, ¡pero esto es más eficaz! Porque usa un hashmap / set:

const customStringify = function (v) {
  const cache = new Set();
  return JSON.stringify(v, function (key, value) {
    if (typeof value === 'object' && value !== null) {
      if (cache.has(value)) {
        // Circular reference found
        try {
          // If this value does not reference a parent it can be deduped
         return JSON.parse(JSON.stringify(value));
        }
        catch (err) {
          // discard key if value cannot be deduped
         return;
        }
      }
      // Store value in our set
      cache.add(value);
    }
    return value;
  });
};

Para objetos profundamente anidados con referencias circulares, intente stringifyDeep => github.com/ORESoftware/safe-stringify
Alexander Mills

Es posible que la implementación de Set solo use una matriz e indexOf debajo del capó, pero no lo he confirmado.
Alexander Mills

Esto es eliminar nodos principales que tienen nodos secundarios incluso con valores diferentes, por ejemplo, {"a":{"b":{"a":"d"}}}e incluso eliminar nodos que tienen un objeto vacío {}
Sandip Pingle

¿Puedes mostrar un ejemplo de ese Sandip? crear un gist.github.com o lo que sea
Alexander Mills

Excelente !!! Primero (desde arriba, pero comprobando solo 2-3 soluciones funcionales) solución de trabajo aquí en node.js y Fission ;-) - bibliotecas colgadas.
Tom

37

Tenga en cuenta que también hay un JSON.decyclemétodo implementado por Douglas Crockford. Ver su ciclo.js . Esto le permite stringificar casi cualquier estructura estándar:

var a = [];
a[0] = a;
a[1] = 123;
console.log(JSON.stringify(JSON.decycle(a)));
// result: '[{"$ref":"$"},123]'.

También puede recrear objetos originales con retrocycle método. Por lo tanto, no tiene que eliminar ciclos de objetos para encadenarlos.

Sin embargo, esto no funcionará para los nodos DOM (que son la causa típica de los ciclos en los casos de uso de la vida real). Por ejemplo, esto arrojará:

var a = [document.body];
console.log(JSON.stringify(JSON.decycle(a)));

He hecho una bifurcación para resolver ese problema (vea mi bifurcación cycle.js ). Esto debería funcionar bien:

var a = [document.body];
console.log(JSON.stringify(JSON.decycle(a, true)));

Tenga en cuenta que en mi tenedor JSON.decycle(variable) funciona como en el original y arrojará una excepción cuando variablecontenga nodos / elementos DOM.

Cuando lo usa JSON.decycle(variable, true), acepta el hecho de que el resultado no será reversible (retrocycle no volverá a crear nodos DOM). Sin embargo, los elementos DOM deben ser identificables hasta cierto punto. Por ejemplo, si un divelemento tiene una identificación, se reemplazará con una cadena "div#id-of-the-element".


2
Tanto su código como el tuyo me dan un "RangeError: se ha excedido el tamaño máximo de la pila de llamadas" cuando los uso.
jcollum

Puedo echar un vistazo si proporciona su código en Fiddle o agrega un problema en Github: github.com/Eccenux/JSON-js/issues
Nux

Esto es lo que estaba buscando. JSON.decycle(a, true)qué sucede cuando pasa true como parámetro para descifrar la función.
Rudra

@Rudra true hace que la stringifyNodesopción sea verdadera en la bifurcación. Esto volcará por ejemplo divcon id = "alguna-id" de la cadena: div#some-id. Evitará algunos problemas, pero no podrá realizar un ciclo completo.
Nux

Hay un paquete npm npmjs.com/package/json-js , pero no se actualizó por un tiempo
Michael Freidgeim

23

Recomiendo revisar json-stringify-safe desde @ isaacs-- se usa en NPM.

Por cierto, si no está utilizando Node.js, puede copiar y pegar las líneas 4-27 de la parte relevante del código fuente .

Instalar:

$ npm install json-stringify-safe --save

Usar:

// Require the thing
var stringify = require('json-stringify-safe');

// Take some nasty circular object
var theBigNasty = {
  a: "foo",
  b: theBigNasty
};

// Then clean it up a little bit
var sanitized = JSON.parse(stringify(theBigNasty));

Esto produce:

{
  a: 'foo',
  b: '[Circular]'
}

Tenga en cuenta que, al igual que con la función vainilla JSON.stringify como se menciona en @Rob W, también puede personalizar el comportamiento de desinfección pasando una función de "reemplazo" como segundo argumento para stringify(). Si necesita un ejemplo simple de cómo hacer esto, acabo de escribir un sustituto personalizado que coacciona errores, expresiones regulares y funciones en cadenas legibles aquí .


13

Para futuros googlers que busquen una solución a este problema cuando no conozcan las claves de todas las referencias circulares, podrían usar un contenedor alrededor de la función JSON.stringify para descartar referencias circulares. Vea un script de ejemplo en https://gist.github.com/4653128 .

La solución esencialmente se reduce a mantener una referencia a objetos previamente impresos en una matriz y verificar eso en una función de reemplazo antes de devolver un valor. Es más restrictivo que solo descartar referencias circulares, porque también descarta imprimir un objeto dos veces, uno de los efectos secundarios es evitar las referencias circulares.

Envoltorio de ejemplo:

function stringifyOnce(obj, replacer, indent){
    var printedObjects = [];
    var printedObjectKeys = [];

    function printOnceReplacer(key, value){
        var printedObjIndex = false;
        printedObjects.forEach(function(obj, index){
            if(obj===value){
                printedObjIndex = index;
            }
        });

        if(printedObjIndex && typeof(value)=="object"){
            return "(see " + value.constructor.name.toLowerCase() + " with key " + printedObjectKeys[printedObjIndex] + ")";
        }else{
            var qualifiedKey = key || "(empty key)";
            printedObjects.push(value);
            printedObjectKeys.push(qualifiedKey);
            if(replacer){
                return replacer(key, value);
            }else{
                return value;
            }
        }
    }
    return JSON.stringify(obj, printOnceReplacer, indent);
}

3
Buen código Sin embargo, tiene un error tonto, escribe if(printedObjIndex)mientras debería escribir if(printedObjIndex==false)porque indextambién puede ser a lo 0que se traduce a falsemenos que indique explícitamente lo contrario.
chico mograbi

1
@guymograbi ¿No quieres decir ===? 0 == falsees true, 0 === falsees false. ; ^) Pero preferiría no inicializar printedObjIndexa falso, ya que entonces puede comprobarlo undefinedpara que (bueno, Trindaz) no mezcle metáforas tan extrañamente.
ruffin

@ruffin buena captura. Sí, obviamente, siempre use la igualdad dura y jshint para atrapar errores tan tontos.
chico mograbi

4

Use el método JSON.stringify con un sustituto. Lea esta documentación para más información. http://msdn.microsoft.com/en-us/library/cc836459%28v=vs.94%29.aspx

var obj = {
  a: "foo",
  b: obj
}

var replacement = {"b":undefined};

alert(JSON.stringify(obj,replacement));

Encuentre una manera de llenar la matriz de reemplazo con referencias cíclicas. Puede usar el método typeof para encontrar si la propiedad es del tipo 'objeto' (referencia) y una verificación de igualdad exacta (===) para verificar la referencia circular.


44
Esto podría funcionar solo en IE (considerando el hecho de que MSDN es documentación de Microsoft y Microsoft crea IE). En Firefox / Chrome, jsfiddle.net/ppmaW genera el error de referencia circular. FYI: var obj = {foo:obj}no no crear una referencia circular. En su lugar, crea un objeto cuyo fooatributo se refiere al valor anterior de obj( undefinedsi no se ha definido previamente, declarado debido a var obj).
Rob W

4

Si

console.log(JSON.stringify(object));

resulta en un

TypeError: valor de objeto cíclico

Entonces es posible que desee imprimir así:

var output = '';
for (property in object) {
  output += property + ': ' + object[property]+'; ';
}
console.log(output);

21
¿Quizás porque solo imprime un nivel?
Alex Turpin

MUY SENCILLO lo voté porque funcionó para mí en cromo. EXCELENTE
Amor y paz - Joe Codeswell

4
var a={b:"b"};
a.a=a;
JSON.stringify(preventCircularJson(a));

evalúa a:

"{"b":"b","a":"CIRCULAR_REFERENCE_REMOVED"}"

con la función:

/**
 * Traverses a javascript object, and deletes all circular values
 * @param source object to remove circular references from
 * @param censoredMessage optional: what to put instead of censored values
 * @param censorTheseItems should be kept null, used in recursion
 * @returns {undefined}
 */
function preventCircularJson(source, censoredMessage, censorTheseItems) {
    //init recursive value if this is the first call
    censorTheseItems = censorTheseItems || [source];
    //default if none is specified
    censoredMessage = censoredMessage || "CIRCULAR_REFERENCE_REMOVED";
    //values that have allready apeared will be placed here:
    var recursiveItems = {};
    //initaite a censored clone to return back
    var ret = {};
    //traverse the object:
    for (var key in source) {
        var value = source[key]
        if (typeof value == "object") {
            //re-examine all complex children again later:
            recursiveItems[key] = value;
        } else {
            //simple values copied as is
            ret[key] = value;
        }
    }
    //create list of values to censor:
    var censorChildItems = [];
    for (var key in recursiveItems) {
        var value = source[key];
        //all complex child objects should not apear again in children:
        censorChildItems.push(value);
    }
    //censor all circular values
    for (var key in recursiveItems) {
        var value = source[key];
        var censored = false;
        censorTheseItems.forEach(function (item) {
            if (item === value) {
                censored = true;
            }
        });
        if (censored) {
            //change circular values to this
            value = censoredMessage;
        } else {
            //recursion:
            value = preventCircularJson(value, censoredMessage, censorChildItems.concat(censorTheseItems));
        }
        ret[key] = value

    }

    return ret;
}

3

Sé que esta es una vieja pregunta, pero me gustaría sugerir un paquete NPM que he creado llamado smart-circular , que funciona de manera diferente a las otras formas propuestas. Es especialmente útil si está utilizando objetos grandes y profundos .

Algunas características son:

  • Reemplazar referencias circulares o simplemente estructuras repetidas dentro del objeto por la ruta que conduce a su primera aparición (no solo la cadena [circular] );

  • Al buscar circularidades en una búsqueda de amplitud, el paquete asegura que este camino sea lo más pequeño posible, lo cual es importante cuando se trata de objetos muy grandes y profundos, donde los caminos pueden ser molestamente largos y difíciles de seguir (el reemplazo personalizado en JSON.stringify hace un DFS);

  • Permite reemplazos personalizados, útiles para simplificar o ignorar partes menos importantes del objeto;

  • Finalmente, las rutas se escriben exactamente de la manera necesaria para acceder al campo al que se hace referencia, lo que puede ayudarlo a depurar.


3

El segundo argumento para JSON.stringify () también permite especificar una matriz de nombres de clave que deben conservarse de cada objeto que encuentre dentro de sus datos. Esto puede no funcionar para todos los casos de uso, pero es una solución mucho más simple.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

var obj = {
    a: "foo",
    b: this
}

var json = JSON.stringify(obj, ['a']);
console.log(json);
// {"a":"foo"}

Nota: Curiosamente, la definición de objeto de OP no arroja un error de referencia circular en el último Chrome o Firefox. La definición de esta respuesta se modificó de forma que lo hizo generará un error.



Esta debe ser aceptada respuesta
Depresión maníaca

2

Para actualizar la respuesta de anular la forma en que funciona JSON (probablemente no recomendado, pero súper simple), no lo use circular-json(está en desuso). En su lugar, use el sucesor, aplanado:

https://www.npmjs.com/package/flatted

Tomado de la antigua respuesta anterior de @ user1541685, pero reemplazada por la nueva:

npm i --save flatted

entonces en tu archivo js

const CircularJSON = require('flatted');
const json = CircularJSON.stringify(obj);

1

Encontré la biblioteca circular-json en github y funcionó bien para mi problema.

Algunas buenas características que encontré útiles:

  • Admite el uso multiplataforma, pero hasta ahora solo lo probé con node.js.
  • API es la misma, por lo que todo lo que necesita hacer es incluirlo y usarlo como reemplazo de JSON.
  • Tiene su propio método de análisis para que pueda convertir los datos serializados 'circulares' de nuevo a objeto.

2
Esta biblioteca arrojó un error para mí, así que tengo que buscar otro. ERROR TypeError: toISOString no es una función en String.toJSON (<anonymous>) en Object. <anonymous> ( localhost: 8100 / build / polyfills.js: 1: 3458 ) en JSON.stringify (<anonymous>) en Object. stringifyRecursion [como stringify] ( localhost: 8100 / build / main.js: 258450: 15 )
Mark Ellul

1
@ MarkEllul He escrito el comentario en 2015 y si veo una alternativa mejor, la publicaré aquí con una edición. Todavía encuentro el mismo problema en el trabajo diario de vez en cuando y generalmente prefiero mis propias funciones manuales de manera recursiva con una inspección adecuada / segura. Sugeriría que consulte las prácticas de programación funcional si no está familiarizado, por lo general, está facilitando este tipo de operaciones recursivas como menos complicado y más confiable.
JacopKane

También obtener "toISOString no es una función" tratando de clasificar en cadena un evento y volver a enviarlo en una prueba de ciprés
Devin G Rhode

1

Resuelvo este problema así:

var util = require('util');

// Our circular object
var obj = {foo: {bar: null}, a:{a:{a:{a:{a:{a:{a:{hi: 'Yo!'}}}}}}}};
obj.foo.bar = obj;

// Generate almost valid JS object definition code (typeof string)
var str = util.inspect(b, {depth: null});

// Fix code to the valid state (in this example it is not required, but my object was huge and complex, and I needed this for my case)
str = str
    .replace(/<Buffer[ \w\.]+>/ig, '"buffer"')
    .replace(/\[Function]/ig, 'function(){}')
    .replace(/\[Circular]/ig, '"Circular"')
    .replace(/\{ \[Function: ([\w]+)]/ig, '{ $1: function $1 () {},')
    .replace(/\[Function: ([\w]+)]/ig, 'function $1(){}')
    .replace(/(\w+): ([\w :]+GMT\+[\w \(\)]+),/ig, '$1: new Date("$2"),')
    .replace(/(\S+): ,/ig, '$1: null,');

// Create function to eval stringifyed code
var foo = new Function('return ' + str + ';');

// And have fun
console.log(JSON.stringify(foo(), null, 4));

Esto funcionó bastante para mí, pero parece que las clases se representaban como _class: ClassName { data: "here" }, así que agregué la siguiente regla .replace(/(\w+) {/g, '{ __ClassName__: "$1", '). En mi caso, estaba tratando de ver cómo se veía un objeto de solicitud http.
redbmk

1

Sé que esta pregunta es antigua y tiene muchas respuestas excelentes, pero publico esta respuesta debido a su nuevo sabor (es5 +)


1

Aunque esto se ha respondido lo suficiente, también puede eliminar explícitamente la propiedad en cuestión antes de la cadena mediante el deleteoperador.

delete obj.b; 
const jsonObject = JSON.stringify(obj);

eliminar operador

Esto eliminará la necesidad de construir o mantener una lógica compleja para eliminar referencias circulares.


1
function myStringify(obj, maxDeepLevel = 2) {
  if (obj === null) {
    return 'null';
  }
  if (obj === undefined) {
    return 'undefined';
  }
  if (maxDeepLevel < 0 || typeof obj !== 'object') {
    return obj.toString();
  }
  return Object
    .entries(obj)
    .map(x => x[0] + ': ' + myStringify(x[1], maxDeepLevel - 1))
    .join('\r\n');
}


0

Basado en las otras respuestas termino con el siguiente código. Funciona bastante bien con referencias circulares, objetos con constructores personalizados.

Del objeto dado a ser serializado,

  • Guarde en caché todos los objetos con los que se encuentre mientras atraviesa el objeto y asigne a cada uno de ellos un hashID único (un número de incremento automático también funciona)
  • Una vez que se encuentra una referencia circular, marque ese campo en el nuevo objeto como circular y almacene el hashID del objeto original como un atributo.

Enlace Github - DecycledJSON

DJSHelper = {};
DJSHelper.Cache = [];
DJSHelper.currentHashID = 0;
DJSHelper.ReviveCache = [];

// DOES NOT SERIALIZE FUNCTION
function DJSNode(name, object, isRoot){
    this.name = name;
    // [ATTRIBUTES] contains the primitive fields of the Node
    this.attributes = {};

    // [CHILDREN] contains the Object/Typed fields of the Node
    // All [CHILDREN] must be of type [DJSNode]
    this.children = []; //Array of DJSNodes only

    // If [IS-ROOT] is true reset the Cache and currentHashId
    // before encoding
    isRoot = typeof isRoot === 'undefined'? true:isRoot;
    this.isRoot = isRoot;
    if(isRoot){
        DJSHelper.Cache = [];
        DJSHelper.currentHashID = 0;

        // CACHE THE ROOT
        object.hashID = DJSHelper.currentHashID++;
        DJSHelper.Cache.push(object);
    }

    for(var a in object){
        if(object.hasOwnProperty(a)){
            var val = object[a];

            if (typeof val === 'object') {
                // IF OBJECT OR NULL REF.

                /***************************************************************************/
                // DO NOT REMOVE THE [FALSE] AS THAT WOULD RESET THE [DJSHELPER.CACHE]
                // AND THE RESULT WOULD BE STACK OVERFLOW
                /***************************************************************************/
                if(val !== null) {
                    if (DJSHelper.Cache.indexOf(val) === -1) {
                        // VAL NOT IN CACHE
                        // ADD THE VAL TO CACHE FIRST -> BEFORE DOING RECURSION
                        val.hashID = DJSHelper.currentHashID++;
                        //console.log("Assigned", val.hashID, "to", a);
                        DJSHelper.Cache.push(val);

                        if (!(val instanceof Array)) {
                            // VAL NOT AN [ARRAY]
                            try {
                                this.children.push(new DJSNode(a, val, false));
                            } catch (err) {
                                console.log(err.message, a);
                                throw err;
                            }
                        } else {
                            // VAL IS AN [ARRAY]
                            var node = new DJSNode(a, {
                                array: true,
                                hashID: val.hashID // HashID of array
                            }, false);
                            val.forEach(function (elem, index) {
                                node.children.push(new DJSNode("elem", {val: elem}, false));
                            });
                            this.children.push(node);
                        }
                    } else {
                        // VAL IN CACHE
                        // ADD A CYCLIC NODE WITH HASH-ID
                        this.children.push(new DJSNode(a, {
                            cyclic: true,
                            hashID: val.hashID
                        }, false));
                    }
                }else{
                    // PUT NULL AS AN ATTRIBUTE
                    this.attributes[a] = 'null';
                }
            } else if (typeof val !== 'function') {
                // MUST BE A PRIMITIVE
                // ADD IT AS AN ATTRIBUTE
                this.attributes[a] = val;
            }
        }
    }

    if(isRoot){
        DJSHelper.Cache = null;
    }
    this.constructorName = object.constructor.name;
}
DJSNode.Revive = function (xmlNode, isRoot) {
    // Default value of [isRoot] is True
    isRoot = typeof isRoot === 'undefined'?true: isRoot;
    var root;
    if(isRoot){
        DJSHelper.ReviveCache = []; //Garbage Collect
    }
    if(window[xmlNode.constructorName].toString().indexOf('[native code]') > -1 ) {
        // yep, native in the browser
        if(xmlNode.constructorName == 'Object'){
            root = {};
        }else{
            return null;
        }
    }else {
        eval('root = new ' + xmlNode.constructorName + "()");
    }

    //CACHE ROOT INTO REVIVE-CACHE
    DJSHelper.ReviveCache[xmlNode.attributes.hashID] = root;

    for(var k in xmlNode.attributes){
        // PRIMITIVE OR NULL REF FIELDS
        if(xmlNode.attributes.hasOwnProperty(k)) {
            var a = xmlNode.attributes[k];
            if(a == 'null'){
                root[k] = null;
            }else {
                root[k] = a;
            }
        }
    }

    xmlNode.children.forEach(function (value) {
        // Each children is an [DJSNode]
        // [Array]s are stored as [DJSNode] with an positive Array attribute
        // So is value

        if(value.attributes.array){
            // ITS AN [ARRAY]
            root[value.name] = [];
            value.children.forEach(function (elem) {
                root[value.name].push(elem.attributes.val);
            });
            //console.log("Caching", value.attributes.hashID);
            DJSHelper.ReviveCache[value.attributes.hashID] = root[value.name];
        }else if(!value.attributes.cyclic){
            // ITS AN [OBJECT]
            root[value.name] = DJSNode.Revive(value, false);
            //console.log("Caching", value.attributes.hashID);
            DJSHelper.ReviveCache[value.attributes.hashID] = root[value.name];
        }
    });

    // [SEPARATE ITERATION] TO MAKE SURE ALL POSSIBLE
    // [CYCLIC] REFERENCES ARE CACHED PROPERLY
    xmlNode.children.forEach(function (value) {
        // Each children is an [DJSNode]
        // [Array]s are stored as [DJSNode] with an positive Array attribute
        // So is value

        if(value.attributes.cyclic){
            // ITS AND [CYCLIC] REFERENCE
            root[value.name] = DJSHelper.ReviveCache[value.attributes.hashID];
        }
    });

    if(isRoot){
        DJSHelper.ReviveCache = null; //Garbage Collect
    }
    return root;
};

DecycledJSON = {};
DecycledJSON.stringify = function (obj) {
    return JSON.stringify(new DJSNode("root", obj));
};
DecycledJSON.parse = function (json, replacerObject) {
    // use the replacerObject to get the null values
    return DJSNode.Revive(JSON.parse(json));
};
DJS = DecycledJSON;

Ejemplo de uso 1:

var obj = {
    id:201,
    box: {
        owner: null,
        key: 'storm'
    },
    lines:[
        'item1',
        23
    ]
};

console.log(obj); // ORIGINAL

// SERIALIZE AND THEN PARSE
var jsonObj = DJS.stringify(obj);
console.log(DJS.parse(jsonObj));

Ejemplo de uso 2:

// PERSON OBJECT

function Person() {
    this.name = null;
    this.child = null;
    this.dad = null;
    this.mom = null;
}
var Dad = new Person();
Dad.name = 'John';
var Mom = new Person();
Mom.name = 'Sarah';
var Child = new Person();
Child.name = 'Kiddo';

Dad.child = Mom.child = Child;
Child.dad = Dad;
Child.mom = Mom;

console.log(Child); // ORIGINAL

// SERIALIZE AND THEN PARSE
var jsonChild = DJS.stringify(Child);
console.log(DJS.parse(jsonChild));

0

Prueba esto:

var obj = {
    a: "foo",
    b: obj
};

var circular_replacer = (value) => {
    var seen = [];
    if (value != null && typeof value == "object") {
        if (seen.indexOf(value) >= 0) return;
        seen.push(value);
    }
    return value;
};

obj = circular_replacer(obj);

¿No debería haber, como, algunas líneas más de código después de seen.push(value)= -D? Me gustafor (var key in value) {value[key] = circular_replacer(value[key]);}
Klesun

Se desaconsejan las respuestas de solo código. Haga clic en editar y agregue algunas palabras que resuman cómo su código aborda la pregunta, o tal vez explique cómo su respuesta difiere de la respuesta / respuestas anteriores. De la opinión
Nick

0

En mi solución, si te encuentras con un ciclo, no solo dice "ciclo" (o nada), dice algo como foo: mira el objeto # 42 arriba, y para ver dónde apunta foo puedes desplazarte hacia arriba y buscar para el objeto # 42 (cada objeto, cuando comienza, dice el objeto # xxx con algún entero xxx)

Retazo:

(function(){
	"use strict";
	var ignore = [Boolean, Date, Number, RegExp, String];
	function primitive(item){
		if (typeof item === 'object'){
			if (item === null) { return true; }
			for (var i=0; i<ignore.length; i++){
				if (item instanceof ignore[i]) { return true; }
			}
			return false;
		} else {
			return true;
		}
	}
	function infant(value){
		return Array.isArray(value) ? [] : {};
	}
	JSON.decycleIntoForest = function decycleIntoForest(object, replacer) {
		if (typeof replacer !== 'function'){
			replacer = function(x){ return x; }
		}
		object = replacer(object);
		if (primitive(object)) return object;
		var objects = [object];
		var forest  = [infant(object)];
		var bucket  = new WeakMap(); // bucket = inverse of objects 
		bucket.set(object, 0);       // i.e., map object to index in array
		function addToBucket(obj){
			var result = objects.length;
			objects.push(obj);
			bucket.set(obj, result);
			return result;
		}
		function isInBucket(obj){
			return bucket.has(obj);
			// objects[bucket.get(obj)] === obj, iff true is returned
		}
		function processNode(source, target){
			Object.keys(source).forEach(function(key){
				var value = replacer(source[key]);
				if (primitive(value)){
					target[key] = {value: value};
				} else {
					var ptr;
					if (isInBucket(value)){
						ptr = bucket.get(value);
					} else {
						ptr = addToBucket(value);
						var newTree = infant(value);
						forest.push(newTree);
						processNode(value, newTree);
					}
					target[key] = {pointer: ptr};
				}
			});
		}
		processNode(object, forest[0]);
		return forest;
	};
})();
the = document.getElementById('the');
function consoleLog(toBeLogged){
  the.textContent = the.textContent + '\n' + toBeLogged;
}
function show(root){
	var cycleFree = JSON.decycleIntoForest(root);
	var shown = cycleFree.map(function(tree, idx){ return false; });
	var indentIncrement = 4;
	function showItem(nodeSlot, indent, label){
	  leadingSpaces = ' '.repeat(indent);
      leadingSpacesPlus = ' '.repeat(indent + indentIncrement);
	  if (shown[nodeSlot]){
	  consoleLog(leadingSpaces + label + ' ... see above (object #' + nodeSlot + ')');
        } else {
		  consoleLog(leadingSpaces + label + ' object#' + nodeSlot);
		  var tree = cycleFree[nodeSlot];
		  shown[nodeSlot] = true;
		  Object.keys(tree).forEach(function(key){
			var entry = tree[key];
			if ('value' in entry){
			  consoleLog(leadingSpacesPlus + key + ": " + entry.value);
                } else {
					if ('pointer' in entry){
						showItem(entry.pointer, indent+indentIncrement, key);
                    }
                }
			});
        }
    }
	showItem(0, 0, 'root');
}
cities4d = {
	Europe:{
		north:[
			{name:"Stockholm", population:1000000, temp:6},
			{name:"Helsinki", population:650000, temp:7.6}
		],
		south:[
			{name:"Madrid", population:3200000, temp:15},
			{name:"Rome", population:4300000, temp:15}
		]
	},
	America:{
		north:[
			{name:"San Francisco", population:900000, temp:14},
			{name:"Quebec", population:530000, temp:4}
		],
		south:[
			{name:"Rio de Janeiro", population:7500000, temp:24},
			{name:"Santiago", population:6300000, temp:14}
		]
	},
	Asia:{
		north:[
			{name:"Moscow", population:13200000, temp:6}
		]
	}
};
cities4d.Europe.north[0].alsoStartsWithS = cities4d.America.north[0];
cities4d.Europe.north[0].smaller = cities4d.Europe.north[1];
cities4d.Europe.south[1].sameLanguage = cities4d.America.south[1];
cities4d.Asia.ptrToRoot = cities4d;
show(cities4d)
<pre id="the"></pre>

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.