Como posso imprimir uma estrutura circular em um formato semelhante a JSON?

680

Eu tenho um grande objeto que desejo converter para JSON e enviar. No entanto, possui estrutura circular. Quero lançar todas as referências circulares existentes e enviar o que puder ser especificado. Como faço isso?

Obrigado.

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

Quero restringir obj em:

{"a":"foo"}
atormentar
fonte
5
Você poderia postar um objeto de amostra com uma referência circular que gostaria de analisar?
TWickz 23/07/12
3
algo como isso ?
Alvin Wong
1
possível duplicado do objecto serialização que contém valor objecto cíclico
Oleg V. Volkov
2
Tarde para a festa, mas há um projeto no github para lidar com isso.
Preston S
questão intimamente relacionada: stackoverflow.com/questions/23117470/…
mathheadinclouds 11/11/19

Respostas:

605

Use JSON.stringifycom um substituto personalizado. Por exemplo:

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

O substituto neste exemplo não está 100% correto (dependendo da sua definição de "duplicado"). No seguinte caso, um valor é descartado:

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, ...);

Mas o conceito permanece: use um substituto personalizado e acompanhe os valores do objeto analisado.

Como uma função utilitária escrita em 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))
Rob W
fonte
1
@ Harry Qual é o problema? Terei o prazer de corrigir a resposta, se houver alguma imprecisão.
23712 Rob Rob W
1
@CruzDiablo Serializing DOM geralmente não faz sentido. No entanto, se você puder pensar em um método de serialização significativo para seus propósitos, tente adicionar um serializado personalizado aos objetos DOM: Node.prototype.toJSON = function() { return 'whatever you think that is right'; };(se desejar algo mais genérico / específico, tente algo na árvore de protótipos: HTMLDivElement implementa implementos HTMLElement implementos elemento Node implementos EventTarget; nota: isto pode ser dependente do navegador, a árvore anterior é verdadeiro para Chrome)
Rob W
7
isso está errado, porque ignorará a segunda aparência de objetos que estão contidos duas vezes, mesmo que não em uma estrutura realmente cíclica. var a={id:1}; JSON.stringify([a,a]);
precisa saber é o seguinte
3
@ user2451227 "O substituto neste exemplo não está 100% correto (dependendo da sua definição de" duplicado "). Mas o conceito permanece: use um substituto personalizado e acompanhe os valores do objeto analisado."
Rob W
4
A preocupação do GC aqui é sem dúvida redundante. Se isso for executado como um script único, o script será encerrado imediatamente. Se isso estiver encapsulado dentro de uma função para implementação, cacheserá inacessível developer.mozilla.org/en-US/docs/Web/JavaScript/…
Trindaz
704

No Node.js, você pode usar o utilitário.inspect (objeto) . Ele substitui automaticamente os links circulares por "[Circular]".


Apesar de estar embutido (nenhuma instalação é necessária) , você deve importá-lo

import * as util from 'util' // has no default export
import { inspect } from 'util' // or directly
// or 
var util = require('util')
Para usá-lo, basta chamar
console.log(util.inspect(myObject))

Lembre-se também de que você pode passar o objeto de opções para inspecionar (veja o link acima)

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



Por favor, leia e dê elogios aos comentaristas abaixo ...

Erel Segal-Halevi
fonte
134
O util é um módulo embutido, você não precisa instalá-lo.
Mitar
10
console.log (util.inspect (obj))
starsinmypockets 25/11/14
19
@Mitar é built-in, mas você ainda tem que carregar o módulovar util = require('util');
Bodecker
14
Não seja idiota como eu, é justo obj_str = util.inspect(thing) , NÃO <s> garbage_str = JSON.stringify(util.inspect(thing))</s> #
ThorSummoner 21/16/16
7
Isso é muito melhor do que mexer com os tipos de verificação. Por que o stringify não pode funcionar apenas assim? Se ele sabe que há uma referência circular, por que simplesmente não pode ser dito para ignorá-la ???
Chris Peacock
141

Gostaria de saber por que ninguém postou a solução adequada da página MDN ainda ...

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

Os valores vistos devem ser armazenados em um conjunto , não na matriz (o substituto é chamado em todos os elementos ) e não há necessidade de tentar JSON.stringify cada elemento da cadeia que leva a uma referência circular.

Como na resposta aceita, esta solução remove todos os valores repetidos , não apenas os circulares. Mas pelo menos não tem complexidade exponencial.

Klesun
fonte
Legal, mas este é apenas o ES2015. Não há suporte para o IE.
Martin Capodici 9/04/19
43
Yoda diz: "Se ainda estiver suportando o IE, use um transpilador que deveria".
Spain Train
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)retorna undefinedno chrome
roberto tomás
1
Funciona em React + Typcript. graças
user3417479
76

apenas faça

npm i --save circular-json

então no seu arquivo js

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

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

NOTA: Não tenho nada a ver com este pacote. Mas eu uso para isso.

Atualização 2020

Observe que o CircularJSON está apenas em manutenção e o seu sucessor é achatado .

user1541685
fonte
Muito obrigado! Ótima biblioteca, economizou muito tempo. Super pequeno (apenas 1,4 KB reduzido).
Brian Haak
16
Eu acho que você pode precisar de mais alguma justificativa para usar um módulo do que "apenas faça". E não é ótimo substituir JSONpor princípio.
Edwin
Eu precisava copiar um objeto para usar no teste de stub. Essa resposta foi perfeita. Copiei o objeto e removi a substituição. Obrigado!!
Chris afiada
1
Segundo o autor, este pacote foi preterido. O CircularJSON está apenas em manutenção, o flatted é o seu sucessor. Link: github.com/WebReflection/flatted#flatted
Robert Molina
3
Cuidado, o pacote 'flatted' (e circular-json?) Não replica a funcionalidade JSON.stringify (). Ele cria seu próprio formato não JSON. (por exemplo, Flatted.stringify({blah: 1})resulta em [{"blah":1}]) vejo alguém tentando levantar uma questão sobre isso, e o autor repreendeu-a e bloqueou a questão para comentários.
Jameslol #
48

Eu realmente gostei da solução do Trindaz - mais detalhada, porém tinha alguns bugs. Eu os consertei para quem gosta também.

Além disso, adicionei um limite de comprimento nos meus objetos de cache.

Se o objeto que estou imprimindo for realmente grande - quero dizer infinitamente grande -, quero limitar meu 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);
};
guy mograbi
fonte
Está faltando uma verificação nula nesta linha: return "(see" + (!! value.constructor? Value.constructor.name.toLowerCase (): typeof (value)) + "with key" + PrintedObjectKeys [PrintedObjIndex] ")";
Isak
Terei prazer em adicioná-lo. deixe-me saber o que é anulável, pois tive problemas até agora.
guy mograbi
2
// os navegadores não imprimirão mais de 20K - mas você coloca o limite como 2k. Talvez mudar para o futuro?
Pochen
38

@ A resposta de RobW está correta, mas isso é mais eficiente! Porque ele usa um 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;
  });
};
Alexander Mills
fonte
Para objetos profundamente aninhados com referências circulares, tente stringifyDeep => github.com/ORESoftware/safe-stringify
Alexander Mills
É possível que a implementação Set use apenas uma matriz e um indexOf, mas não confirmei isso.
Alexander Mills
Isso está removendo nós pai com nós filhos, mesmo com valores diferentes - por exemplo - {"a":{"b":{"a":"d"}}}e até removendo nós com objeto vazio {}
Sandip Pingle
Você pode mostrar um exemplo desse Sandip? crie um gist.github.com ou outros enfeites #
Alexander Mills
Excelente !!! Primeira (a partir do topo, mas verificada apenas 2-3 soluções de funções) solução de trabalho aqui em node.js e Fission ;-) - bibliotecas desligadas.
Tom
37

Observe que também há um JSON.decyclemétodo implementado por Douglas Crockford. Veja o seu cycle.js . Isso permite que você especifique quase qualquer estrutura padrão:

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

Você também pode recriar o objeto original com o retrocyclemétodo Portanto, você não precisa remover os ciclos dos objetos para especificá-los.

No entanto, isso não funcionará para nós DOM (que são a causa típica de ciclos em casos de uso da vida real). Por exemplo, isso jogará:

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

Fiz um garfo para resolver esse problema (veja meu garfo cycle.js ). Isso deve funcionar bem:

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

Observe que no meu fork JSON.decycle(variable)funciona como no original e lançará uma exceção quando os variablenós / elementos do DOM contiverem.

Ao usar, JSON.decycle(variable, true)você aceita o fato de que o resultado não será reversível (o retrociclo não recriará os nós do DOM). Os elementos DOM devem ser identificados até certo ponto. Por exemplo, se um divelemento tiver um ID, ele será substituído por uma sequência "div#id-of-the-element".

Nux
fonte
2
Tanto o código dele como o seu me fornecem um "RangeError: tamanho máximo da pilha de chamadas excedido" quando eu os uso.
Jcollum
Posso dar uma olhada se você fornecer seu código no Fiddle ou adicionar um problema no Github: github.com/Eccenux/JSON-js/issues
Nux
Era isso que eu estava procurando. JSON.decycle(a, true)o que acontece quando você passa true como um parâmetro para reciclar a função.
quer
@Rudra true torna a stringifyNodesopção verdadeira no garfo. Isso irá despejar por exemplo, divcom id = "some-id" a string: div#some-id. Você evitará alguns problemas, mas não poderá retroceder completamente.
Nux
Existe o pacote npm npmjs.com/package/json-js , mas não foi atualizado por um tempo
Michael Freidgeim 18/10/1919
23

Eu recomendo verificar o json-stringify-safe do @ isaacs-- é usado no NPM.

BTW- se você não estiver usando o Node.js, basta copiar e colar as linhas 4-27 da parte relevante do código-fonte .

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

Isso produz:

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

Observe que, assim como a função JSON.stringify da baunilha, como mencionou o @Rob W, você também pode personalizar o comportamento de higienização passando uma função "substituta" como o segundo argumento para stringify(). Se você precisar de um exemplo simples de como fazer isso, acabei de escrever um substituto personalizado que restringe erros, regexps e funções em strings legíveis por humanos aqui .

mikermcneil
fonte
13

Para futuros googlers que procuram uma solução para esse problema quando não conhecem as chaves de todas as referências circulares, você pode usar um wrapper em torno da função JSON.stringify para descartar referências circulares. Veja um script de exemplo em https://gist.github.com/4653128 .

A solução basicamente se resume em manter uma referência a objetos impressos anteriormente em uma matriz e verificar isso em uma função substituta antes de retornar um valor. É mais constritivo do que excluir as referências circulares, porque também descarta a impressão de um objeto duas vezes, um dos efeitos colaterais é o de evitar referências circulares.

Exemplo de wrapper:

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);
}
Trindaz
fonte
3
Código legal. Você tem um erro bobo, porém, escreve if(printedObjIndex)enquanto deve escrever, if(printedObjIndex==false)porque indextambém pode ser 0traduzido para false, a menos que você indique explicitamente o contrário.
cara Mograbi
1
@guymograbi Você não quer dizer ===? 0 == falseé true, 0 === falseé false. ; ^) Mas eu prefiro não inicializar printedObjIndexcomo false, pois então você pode undefinedcomparar para que você (bem, o de Trindaz) não misture metáforas tão estranhamente.
Ruffin
@ruffin boa captura. Sim, obviamente, sempre use igualdade e jshint rígidos para detectar erros tão bobos.
cara Mograbi
4

Use o método JSON.stringify com um substituto. Leia esta documentação para obter mais informações. 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));

Descubra uma maneira de preencher a matriz de substituição com referências cíclicas. Você pode usar o método typeof para descobrir se uma propriedade é do tipo 'objeto' (referência) e uma verificação exata de igualdade (===) para verificar a referência circular.

TWickz
fonte
4
Isso pode funcionar apenas no IE (considerando o fato de que o MSDN é documentação da Microsoft e a Microsoft cria o IE). No Firefox / Chrome, jsfiddle.net/ppmaW gera o erro de referência circular. FYI: var obj = {foo:obj}não não criar uma referência circular. Em vez disso, ele cria um objeto cujo fooatributo se refere ao valor anterior de obj( undefinedse não definido anteriormente, declarado por causa var obj).
Rob W
4

E se

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

resulta em um

TypeError: valor do objeto cíclico

Então você pode imprimir assim:

var output = '';
for (property in object) {
  output += property + ': ' + object[property]+'; ';
}
console.log(output);
Thorsten Niehues
fonte
21
Talvez porque apenas imprima um nível?
Alex Turpin
MUITO SIMPLES votei isso de positivo porque funcionou para mim logo de cara no chrome. EXCELENTE
Amor e paz #
4
var a={b:"b"};
a.a=a;
JSON.stringify(preventCircularJson(a));

avalia como:

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

com a função:

/**
 * 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;
}
eshalev
fonte
3

Sei que essa é uma pergunta antiga, mas gostaria de sugerir um pacote NPM que criei chamado smart-circular , que funciona de maneira diferente das outras maneiras propostas. É especialmente útil se você estiver usando objetos grandes e profundos .

Alguns recursos são:

  • Substituindo referências circulares ou simplesmente estruturas repetidas dentro do objeto pelo caminho que leva à sua primeira ocorrência (não apenas a string [circular] );

  • Ao procurar circularidades em uma pesquisa abrangente, o pacote garante que esse caminho seja o menor possível, o que é importante ao lidar com objetos muito grandes e profundos, nos quais os caminhos podem ser irritantemente longos e difíceis de seguir (a substituição personalizada em JSON.stringify faz um DFS);

  • Permite substituições personalizadas, úteis para simplificar ou ignorar partes menos importantes do objeto;

  • Por fim, os caminhos são escritos exatamente da maneira necessária para acessar o campo referenciado, o que pode ajudá-lo a depurar.

Danilo Augusto
fonte
3

O segundo argumento para JSON.stringify () também permite especificar uma matriz de nomes de chaves que devem ser preservados de todos os objetos encontrados nos seus dados. Isso pode não funcionar para todos os casos de uso, mas é uma solução muito mais simples.

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: Estranhamente, a definição de objeto do OP não gera um erro de referência circular no Chrome ou Firefox mais recente. A definição nesta resposta foi modificada para que ele fez lançar um erro.


Aaron Cicali
fonte
Essa resposta deve ser aceita
Manic Depression
2

Para atualizar a resposta de substituir a maneira como o JSON funciona (provavelmente não recomendado, mas super simples), não use circular-json(está obsoleto). Em vez disso, use o sucessor, achatado:

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

Emprestado da resposta antiga acima de @ user1541685, mas substituído pela nova:

npm i --save flatted

então no seu arquivo js

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

Encontrei a biblioteca circular-json no github e funcionou bem para o meu problema.

Algumas boas características que achei úteis:

  • Oferece suporte ao uso de várias plataformas, mas só o testei com o node.js até o momento.
  • A API é a mesma; portanto, tudo que você precisa fazer é incluir e usá-la como uma substituição JSON.
  • Ele possui seu próprio método de análise, para que você possa converter os dados serializados 'circulares' de volta ao objeto.
JacopKane
fonte
2
Esta biblioteca gerou um erro para mim, então tenho que procurar outra. ERRO TypeError: toISOString não é uma função em String.toJSON (<anonymous>) em Object. <anonymous> ( localhost: 8100 / build / polyfills.js: 1: 3458 ) em JSON.stringify (<anonymous>) em Object. stringifyRecursion [como stringify] ( localhost: 8100 / build / main.js: 258450: 15 )
Mark Ellul
1
@ MarkEllul Escrevi o comentário em 2015 e, se encontrar uma alternativa melhor, publicarei aqui com uma edição. Ainda termino o mesmo problema no trabalho diário ocasionalmente e geralmente prefiro minhas próprias funções manuais de maneira recursiva, com uma inspeção adequada / segura. Eu sugeriria verificar as práticas de programação funcional se você não estiver familiarizado, geralmente, está facilitando esse tipo de operação recursiva por ser menos complicado e mais confiável.
JacopKane 02/06/19
Também obter "toISOString não é uma função" tentando especificar um evento e reenviá-lo em um teste de cipreste
Devin G Rhode
1

Eu resolvo esse problema assim:

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));
MiF
fonte
Isso praticamente funcionou para mim, mas parece que as classes estavam sendo representadas _class: ClassName { data: "here" }, então eu adicionei a regra a seguir .replace(/(\w+) {/g, '{ __ClassName__: "$1", '). No meu caso, eu estava tentando ver como era um objeto de solicitação http.
Redbmk
1

Eu sei que esta pergunta é antiga e tem muitas ótimas respostas, mas eu posto essa resposta por causa de seu novo sabor (es5 +)

Morteza Tourani
fonte
1

Embora isso tenha sido respondido suficientemente, você também pode excluir explicitamente a propriedade em questão antes da stringização usando o deleteoperador.

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

operador de exclusão

isso removerá a necessidade de criar ou manter uma lógica complexa para remover referências circulares.

lachlan.p.jordan
fonte
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');
}
Sergey Gurin
fonte
0

uma outra solução para resolver esse problema com esse tipo de objeto é que o uso dessa biblioteca

https://github.com/ericmuyser/stringy

é simples e você pode, em alguns passos simples, resolver isso.

Ehsan Aghaei
fonte
0

Com base nas outras respostas, acabo com o código a seguir. Funciona muito bem com referências circulares, objetos com construtores personalizados.

Do objeto especificado a ser serializado,

  • Coloque em cache todo o objeto que você encontrar ao atravessá-lo e atribua a cada um deles um hashID exclusivo (um número de incremento automático também funciona)
  • Depois que uma referência circular for encontrada, marque esse campo no novo objeto como circular e armazene o hashID do objeto original como um atributo.

Link do 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;

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

Exemplo 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));
bytestorm
fonte
0

Tente o seguinte:

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);
IamMHussain
fonte
Não deveria haver mais algumas linhas de código após o seen.push(value)= -D? Comofor (var key in value) {value[key] = circular_replacer(value[key]);}
Klesun
As respostas somente de código são desencorajadas. Clique em editar e adicione algumas palavras resumindo como o seu código aborda a pergunta ou talvez explique como a sua resposta difere da resposta / respostas anteriores. Da avaliação
Nick
0

Na minha solução, se você se deparar com um ciclo, ele não diz apenas "ciclo" (ou nada), mas algo como foo: veja o objeto nº 42 acima e para ver onde foo aponta, você pode rolar para cima e pesquisar para o objeto # 42 (cada objeto, quando iniciado, diz o objeto # xxx com algum número inteiro xxx)

Snippet:

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

mathheadinclouds
fonte