Objeto de serialização que contém valor de objeto cíclico

151

Eu tenho um objeto (árvore de análise) que contém nós filhos que são referências a outros nós.

Eu gostaria de serializar esse objeto usando JSON.stringify(), mas recebo

TypeError: valor do objeto cíclico

por causa das construções que mencionei.

Como eu poderia resolver isso? Não me importa se essas referências a outros nós estão representadas ou não no objeto serializado.

Por outro lado, remover essas propriedades do objeto quando elas estão sendo criadas parece entediante e eu não gostaria de fazer alterações no analisador (narciso).

Loic Duros
fonte
1
Não podemos ajudá-lo sem algum código. Poste os bits relevantes do seu objeto e / ou saída JSON junto com o JS que você usa para serializá-lo.
Bojangles
1
você é capaz de adicionar algum prefixo às propriedades que são referências internas?
wheresrhys
@Loic Seria valioso ter a cycle.jsresposta de Douglas Crockford aqui, já que é a solução mais apropriada para muitos casos. Parece apropriado que você poste essa resposta, já que você é o primeiro a fazer referência a ela (no seu comentário abaixo). Se você não quiser publicá-lo como uma resposta, acabarei por fazê-lo.
Jeremy Banks
1
Eu gostaria que o JSON fosse mais inteligente ou uma maneira mais fácil de resolver isso. As soluções são muito problemáticas para fins de depuração simples (!) Imo.
BluE 6/09/19

Respostas:

220

Use o segundo parâmetro de stringify, a função substituta , para excluir objetos já serializados:

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/

Como apontado corretamente em outros comentários, esse código remove todos os objetos "vistos", não apenas os "recursivos".

Por exemplo, para:

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

o resultado estará incorreto. Se sua estrutura for assim, convém usar o decycle de Crockford ou essa função (mais simples) que substitui as referências recursivas por nulos:

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
fonte
3
aaah legal! Obrigado, vou tentar isso. Encontrei uma solução criada por Douglas Crockford ( github.com/douglascrockford/JSON-js/blob/master/cycle.js ), mas como não tenho certeza da licença que a acompanha, a solução fácil que você descreve seria perfeita!
Loic Duros
3
@LoicDuros A licença é "domínio público". Ou seja, você pode fazer o que quiser com ele.
Ates Goral 14/10/12
1
esse código produz loops de ciclo, cuidado com o uso, trava muito seu aplicativo. precisa de ponto-e-vírgula correto e não pode ser usado em objetos de evento!
precisa
3
Isso remove mais do que apenas referências cíclicas - simplesmente remove tudo o que aparece mais de uma vez. A menos que o objeto que já tem sido serializado é um "pai" do novo objeto, você não deve excluí-lo
Gio
1
Boa resposta! Modifiquei um pouco isso, mudei a função para uma função recursiva, para que os objetos filhos fossem clonados da mesma forma que os objetos pais.
HoldOffHunger
2

Eu criei um GitHub Gist que é capaz de detectar estruturas cíclicas e também de- e as codifica: https://gist.github.com/Hoff97/9842228

Para transformar basta usar JSONE.stringify / JSONE.parse. Também decodifica e codifica funções. Se você quiser desativar isso, remova as linhas 32-48 e 61-85.

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

Você pode encontrar um exemplo de violino aqui:

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

Hoff
fonte
2

Essa é uma resposta alternativa, mas como muitas pessoas virão aqui para depurar seus objetos circulares e não há realmente uma ótima maneira de fazer isso sem inserir um monte de código, aqui vai.

Um recurso que não é tão conhecido como JSON.stringify()é console.table(). Basta ligar console.table(whatever);e ele registrará a variável no console em formato de tabela, tornando bastante fácil e conveniente ler o conteúdo da variável.

Andrew
fonte
1

muito mais econômico e mostra onde estava um objeto de ciclo .

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

produz

jsonify = {"g":{"d":[2,5],"j":2},"e":10,"someloopshere":[{"d":[2,5],"j":2},"__cycle__",{"a":[10,"__cycle__"]}]}
Ol Sen
fonte
mas ainda há um problema com este código se alguém iria construir um objeto com obj.b=this'se alguém sabe como evitar muito longas calcs feitas de um determinado escopo de errado com thisseria bom para ver aqui
Ol Sen
2
Isso deve serseen.indexOf(v) != -1
1

Também crio um projeto no github que pode serializar objetos cíclicos e restaurar a classe se você o salvar no atributo serializename como uma 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

Edit: Transformei meu script para NPM https://github.com/bormat/borto_circular_serialize e alterei os nomes das funções de francês para inglês.

Bormat
fonte
Este exemplo não se encaixa no Gist. O Gist tem erros.
Ernst Ernst
Boa ideia - mas uma vez que esteja pronta :-) Se você a distribuir em npm, talvez desenvolva até mesmo tipos para isso, provavelmente se tornou bastante popular.
peterh - Restabelece Monica
1

Aqui está um exemplo de uma estrutura de dados com referências cíclicas: toolshedCY

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

Quando desejar MANTENHA as referências cíclicas (restaurá-los quando você desserializar, em vez de "nuking-los"), você tem 2 opções, o que eu vou comparar aqui. O primeiro é o cycle.js de Douglas Crockford , o segundo é o meu pacote da Sibéria . Ambos trabalham primeiro "deciclando" o objeto, ou seja, construindo outro objeto (sem nenhuma referência cíclica) "contendo a mesma informação".

O Sr. Crockford vai primeiro:

JSON.decycle(makeToolshed())

JSON_decycleMakeToolshed

Como você vê, a estrutura aninhada do JSON é mantida, mas há uma coisa nova, que é objetos com a $refpropriedade especial . Vamos ver como isso funciona.

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

O cifrão representa a raiz. .boltter $refnos diz que .bolté um objeto "já visto" e o valor dessa propriedade especial (aqui, a string $ ["nut"] ["needs"]) nos diz onde, veja primeiro ===acima. Da mesma forma para o segundo $refe o segundo ===acima.

Vamos usar um teste de igualdade profunda adequado (ou seja, a deepGraphEqualfunção de Anders Kaseorg da resposta aceita a esta pergunta ) para ver se a clonagem funciona.

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

Agora, na Sibéria:

JSON.Siberia.forestify(makeToolshed())

JSON_Siberia_forestify_makeToolshed

A Sibéria não tenta imitar JSON "clássico", nenhuma estrutura aninhada. O gráfico do objeto é descrito de maneira "plana". Cada nó do gráfico de objeto é transformado em uma árvore plana (lista de pares de valores de chave simples com valores somente inteiros), que é uma entrada em .forest.No índice zero, encontramos o objeto raiz, em índices mais altos, encontramos os outros nós de o gráfico do objeto e os valores negativos (de alguma chave de alguma árvore da floresta) apontam para a atomsmatriz (que é digitada através da matriz de tipos, mas pularemos os detalhes da digitação aqui). Todos os nós terminais estão na tabela de átomos, todos os nós não terminais estão na tabela de floresta e é possível ver imediatamente quantos nós o gráfico de objetos possui, a saber forest.length. Vamos testar se funciona:

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

comparação

adicionará a seção mais tarde.

mathheadinclouds
fonte
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
    }
  );
}

Faltava uma pré-condição, caso contrário, os valores inteiros nos objetos da matriz são truncados, ou seja, [[08.11.2014 12:30:13, 1095]] 1095 é reduzido para 095.

user3893329
fonte
Obtendo RefrenceError: Não foi possível encontrar a variável: _
amit pandya
Corrija seu código.
Anastasios Moraitis 16/06