Atravessar todos os nós de uma árvore de objetos JSON com JavaScript

148

Gostaria de percorrer uma árvore de objetos JSON, mas não consigo encontrar nenhuma biblioteca para isso. Não parece difícil, mas parece reinventar a roda.

No XML, existem muitos tutoriais mostrando como percorrer uma árvore XML com o DOM :(

Patsy Issa
fonte
1
Tornou o iterador IIFE github.com/eltomjan/ETEhomeTools/blob/master/HTM_HTA/… e predefiniu (básico) o DepthFirst & BreadthFirst a seguir (capacidade básica) e a capacidade de se mover dentro da estrutura JSON sem recursão.
Tom

Respostas:

222

Se você acha que o jQuery é um exagero para uma tarefa tão primitiva, você pode fazer algo assim:

//your object
var o = { 
    foo:"bar",
    arr:[1,2,3],
    subo: {
        foo2:"bar2"
    }
};

//called with every property and its value
function process(key,value) {
    console.log(key + " : "+value);
}

function traverse(o,func) {
    for (var i in o) {
        func.apply(this,[i,o[i]]);  
        if (o[i] !== null && typeof(o[i])=="object") {
            //going one step down in the object tree!!
            traverse(o[i],func);
        }
    }
}

//that's all... no magic, no bloated framework
traverse(o,process);
TheHippo
fonte
2
Por que fund.apply (isso, ...)? Não deveria ser func.apply (o, ...)?
Craig Celeste
4
@ParchedSquid No. Se você consultar os documentos da API para apply (), o primeiro parâmetro é o thisvalor na função de destino, enquanto que odeve ser o primeiro parâmetro para a função. Configurá-lo para this(qual seria a traversefunção) é um pouco estranho, mas não é como se processusasse a thisreferência de qualquer maneira. Poderia muito bem ter sido nulo.
Thor84no
1
Para jshint no modo estrito que você pode precisar adicionar /*jshint validthis: true */acima func.apply(this,[i,o[i]]);para evitar o erro W040: Possible strict violation.causado pelo uso dethis
Jasdeep Khalsa
4
@jasdeepkhalsa: Isso é verdade. Mas, no momento da redação da resposta, o jshint nem sequer começou como um projeto por um ano e meio.
TheHippo
1
@ Visual, você pode adicionar um parâmetro 3 à traversefunção que rastreia a profundidade. Quando chamarmos recursivamente, adicione 1 ao nível atual.
TheHippo
75

Um objeto JSON é simplesmente um objeto Javascript. Na verdade, é isso que JSON significa: JavaScript Object Notation. Portanto, você atravessaria um objeto JSON, no entanto, escolheria "atravessar" um objeto Javascript em geral.

No ES2017, você faria:

Object.entries(jsonObj).forEach(([key, value]) => {
    // do something with key and val
});

Você sempre pode escrever uma função para descer recursivamente ao objeto:

function traverse(jsonObj) {
    if( jsonObj !== null && typeof jsonObj == "object" ) {
        Object.entries(jsonObj).forEach(([key, value]) => {
            // key is either an array index or object key
            traverse(value);
        });
    }
    else {
        // jsonObj is a number or string
    }
}

Este deve ser um bom ponto de partida. Eu recomendo o uso de métodos javascript modernos para essas coisas, pois eles facilitam a escrita desse código.

Eli Courtwright
fonte
9
Evite a travessia (v) em que v == null, porque (typeof null == "object") === true. function traverse(jsonObj) { if(jsonObj && typeof jsonObj == "object" ) { ...
Marcelo Amorim
4
Eu odeio parecer pedante, mas acho que já há muita confusão sobre isso, então, por uma questão de clareza, digo o seguinte. Objetos JSON e JavaScript não são a mesma coisa. O JSON é baseado na formatação de objetos JavaScript, mas o JSON é apenas a notação ; é uma sequência de caracteres representando um objeto. Todos os JSON podem ser "analisados" em um objeto JS, mas nem todos os objetos JS podem ser "stringificados" em JSON. Por exemplo, objetos JS auto-referenciais não podem ser especificados.
John
36
function traverse(o) {
    for (var i in o) {
        if (!!o[i] && typeof(o[i])=="object") {
            console.log(i, o[i]);
            traverse(o[i]);
        } else {
            console.log(i, o[i]);
        }
    }
}
tejas
fonte
6
Você poderia explicar por que é much better?
Dementic
3
Se o método pretende fazer algo diferente de log, você deve procurar nulo, nulo ainda é um objeto.
ICG1
3
@ wi1 Concordo com você, poderia procurar por!!o[i] && typeof o[i] == 'object'
pilau
32

Há uma nova biblioteca para atravessar dados JSON com JavaScript que suporta muitos casos de uso diferentes.

https://npmjs.org/package/traverse

https://github.com/substack/js-traverse

Funciona com todos os tipos de objetos JavaScript. Ele até detecta ciclos.

Ele fornece o caminho de cada nó também.

Benjamin Atkin
fonte
1
js-atravessar também parece estar disponível via npm no node.js.
Ville
Sim. É apenas chamado de atravessar lá. E eles têm uma página da web adorável! Atualizando minha resposta para incluí-la.
Benjamin Atkin
15

Depende do que você quer fazer. Aqui está um exemplo de como percorrer uma árvore de objetos JavaScript, imprimir chaves e valores conforme segue:

function js_traverse(o) {
    var type = typeof o 
    if (type == "object") {
        for (var key in o) {
            print("key: ", key)
            js_traverse(o[key])
        }
    } else {
        print(o)
    }
}

js> foobar = {foo: "bar", baz: "quux", zot: [1, 2, 3, {some: "hash"}]}
[object Object]
js> js_traverse(foobar)                 
key:  foo
bar
key:  baz
quux
key:  zot
key:  0
1
key:  1
2
key:  2
3
key:  3
key:  some
hash
Brian Campbell
fonte
9

Se você estiver atravessando uma string JSON real , poderá usar uma função reviver.

function traverse (json, callback) {
  JSON.parse(json, function (key, value) {
    if (key !== '') {
      callback.call(this, key, value)
    }
    return value
  })
}

traverse('{"a":{"b":{"c":{"d":1}},"e":{"f":2}}}', function (key, value) {
  console.log(arguments)
})

Ao atravessar um objeto:

function traverse (obj, callback, trail) {
  trail = trail || []

  Object.keys(obj).forEach(function (key) {
    var value = obj[key]

    if (Object.getPrototypeOf(value) === Object.prototype) {
      traverse(value, callback, trail.concat(key))
    } else {
      callback.call(obj, key, value, trail)
    }
  })
}

traverse({a: {b: {c: {d: 1}}, e: {f: 2}}}, function (key, value, trail) {
  console.log(arguments)
})
David Lane
fonte
8

EDIT : Todos os exemplos abaixo nesta resposta foram editados para incluir uma nova variável de caminho produzida pelo iterador conforme a solicitação do @ supersan . A variável path é uma matriz de cadeias em que cada cadeia na matriz representa cada chave acessada para obter o valor iterado resultante do objeto de origem original. A variável path pode ser inserida na função / método get do lodash . Ou você pode escrever sua própria versão do get do lodash, que lida apenas com matrizes da seguinte forma:

function get (object, path) {
  return path.reduce((obj, pathItem) => obj ? obj[pathItem] : undefined, object);
}

const example = {a: [1,2,3], b: 4, c: { d: ["foo"] }};
// these paths exist on the object
console.log(get(example, ["a", "0"]));
console.log(get(example, ["c", "d", "0"]));
console.log(get(example, ["b"]));
// these paths do not exist on the object
console.log(get(example, ["e", "f", "g"]));
console.log(get(example, ["b", "f", "g"]));

EDIT : Esta resposta editada resolve percursos de loop infinito.

Parando travessias de objetos infinitos traquinas

Esta resposta editada ainda fornece um dos benefícios adicionais da minha resposta original, que permite que você use a função de gerador fornecida para usar uma interface iterável mais limpa e simples (pense em usar for ofloops como for(var a of b)onde bé um iterável e aé um elemento do iterável ) Usando a função de gerador além de ser uma API simples que também ajuda com a reutilização de código, tornando-a assim que você não tem que repetir a lógica iteração em todos os lugares que você quer iterate profundamente sobre as propriedades do objeto e também torna possível breakfora do o loop se você gostaria de interromper a iteração anteriormente.

Uma coisa que noto que não foi abordada e que não está na minha resposta original é que você deve ter cuidado ao atravessar objetos arbitrários (ou seja, qualquer conjunto "aleatório" de)), porque os objetos JavaScript podem ser auto-referenciados. Isso cria a oportunidade de ter percursos de loop infinitos. Dados JSON não modificados, no entanto, não podem ser auto-referenciados; portanto, se você estiver usando esse subconjunto específico de objetos JS, não precisará se preocupar com percursos de loop infinito e poderá consultar minha resposta original ou outras respostas. Aqui está um exemplo de uma travessia sem fim (observe que não é um trecho de código executável, porque, caso contrário, poderia travar a guia do navegador).

Também no objeto gerador no meu exemplo editado, optei por usar, em Object.keysvez de for initerar apenas chaves não protótipo no objeto. Você pode trocar isso sozinho se quiser incluir as chaves do protótipo. Veja minha seção de resposta original abaixo para ambas as implementações com Object.keyse for in.

Pior - Isso fará um loop infinito em objetos auto-referenciais:

//your object
var o = { 
    foo:"bar",
    arr:[1,2,3],
    subo: {
        foo2:"bar2"
    }
};

// this self-referential property assignment is the only edited line 
// from the below original example which makes the traversal 
// non-terminating (i.e. it makes it infinite loop)
o.o = o;

function* traverse(o, path=[]) {
    for (var i of Object.keys(o)) {
        const itemPath = path.concat(i);
        yield [i,o[i],itemPath]; 
        if (o[i] !== null && typeof(o[i])=="object") {
            //going one step down in the object tree!!
            yield* traverse(o[I], itemPath);
        }
    }
}

//that's all... no magic, no bloated framework
for(var [key, value, path] of traverse(o)) {
  // do something here with each key and value
  console.log(key, value, path);
}

Para se salvar disso, você pode adicionar um conjunto em um fechamento, para que, quando a função for chamada pela primeira vez, comece a criar uma memória dos objetos que viu e não continue a iteração depois de encontrar um objeto já visto. O trecho de código abaixo faz isso e, portanto, lida com casos de loop infinito.

Melhor - Isso não fará um loop infinito em objetos auto-referenciais:

//your object
var o = { 
  foo:"bar",
  arr:[1,2,3],
  subo: {
    foo2:"bar2"
  }
};

// this self-referential property assignment is the only edited line 
// from the below original example which makes more naive traversals 
// non-terminating (i.e. it makes it infinite loop)
o.o = o;

function* traverse(o) {
  const memory = new Set();
  function * innerTraversal (o, path=[]) {
    if(memory.has(o)) {
      // we've seen this object before don't iterate it
      return;
    }
    // add the new object to our memory.
    memory.add(o);
    for (var i of Object.keys(o)) {
      const itemPath = path.concat(i);
      yield [i,o[i],itemPath]; 
      if (o[i] !== null && typeof(o[i])=="object") {
        //going one step down in the object tree!!
        yield* innerTraversal(o[i], itemPath);
      }
    }
  }
    
  yield* innerTraversal(o);
}
console.log(o);
//that's all... no magic, no bloated framework
for(var [key, value, path] of traverse(o)) {
  // do something here with each key and value
  console.log(key, value, path);
}


Resposta original

Para uma maneira mais nova de fazer isso, se você não se importa em deixar o IE e principalmente oferecer suporte a navegadores mais atuais (verifique a tabela es6 da kangax para compatibilidade). Você pode usar os geradores es2015 para isso. Atualizei a resposta do @ TheHippo de acordo. Obviamente, se você realmente deseja suporte ao IE, pode usar o transpiler babel JavaScript.

//your object
var o = { 
    foo:"bar",
    arr:[1,2,3],
    subo: {
        foo2:"bar2"
    }
};

function* traverse(o, path=[]) {
    for (var i in o) {
        const itemPath = path.concat(i);
        yield [i,o[i],itemPath];
        if (o[i] !== null && typeof(o[i])=="object") {
            //going one step down in the object tree!!
            yield* traverse(o[i], itemPath);
        }
    }
}

//that's all... no magic, no bloated framework
for(var [key, value, path] of traverse(o)) {
  // do something here with each key and value
  console.log(key, value, path);
}

Se você deseja apenas propriedades enumeráveis ​​(basicamente propriedades de cadeia que não são protótipos), é possível alterá-lo para iterar usando Object.keysum for...ofloop:

//your object
var o = { 
    foo:"bar",
    arr:[1,2,3],
    subo: {
        foo2:"bar2"
    }
};

function* traverse(o,path=[]) {
    for (var i of Object.keys(o)) {
        const itemPath = path.concat(i);
        yield [i,o[i],itemPath];
        if (o[i] !== null && typeof(o[i])=="object") {
            //going one step down in the object tree!!
            yield* traverse(o[i],itemPath);
        }
    }
}

//that's all... no magic, no bloated framework
for(var [key, value, path] of traverse(o)) {
  // do something here with each key and value
  console.log(key, value, path);
}

John
fonte
Ótima resposta! É possível retornar caminhos como abc, abcd, etc para cada chave que está sendo percorrida?
supersan
1
@supersan, você pode ver meus trechos de código atualizados. Eu adicionei uma variável de caminho para cada uma que é uma matriz de seqüências de caracteres. As cadeias na matriz representam cada chave que foi acessada para obter o valor iterado resultante do objeto de origem original.
John
4

Eu queria usar a solução perfeita do @TheHippo em uma função anônima, sem o uso de funções de processo e gatilho. O seguinte funcionou para mim, compartilhando para programadores iniciantes como eu.

(function traverse(o) {
    for (var i in o) {
        console.log('key : ' + i + ', value: ' + o[i]);

        if (o[i] !== null && typeof(o[i])=="object") {
            //going on step down in the object tree!!
            traverse(o[i]);
        }
    }
  })
  (json);
Raf
fonte
2

A maioria dos mecanismos Javascript não otimiza a recursão da cauda (isso pode não ser um problema se o seu JSON não estiver profundamente aninhado), mas eu geralmente erro por precaução e faço a iteração, por exemplo

function traverse(o, fn) {
    const stack = [o]

    while (stack.length) {
        const obj = stack.shift()

        Object.keys(obj).forEach((key) => {
            fn(key, obj[key], obj)
            if (obj[key] instanceof Object) {
                stack.unshift(obj[key])
                return
            }
        })
    }
}

const o = {
    name: 'Max',
    legal: false,
    other: {
        name: 'Maxwell',
        nested: {
            legal: true
        }
    }
}

const fx = (key, value, obj) => console.log(key, value)
traverse(o, fx)
Máx.
fonte
0

Meu script:

op_needed = [];
callback_func = function(val) {
  var i, j, len;
  results = [];
  for (j = 0, len = val.length; j < len; j++) {
    i = val[j];
    if (i['children'].length !== 0) {
      call_func(i['children']);
    } else {
      op_needed.push(i['rel_path']);
    }
  }
  return op_needed;
};

JSON de entrada:

[
    {
        "id": null, 
        "name": "output",   
        "asset_type_assoc": [], 
        "rel_path": "output",
        "children": [
            {
                "id": null, 
                "name": "output",   
                "asset_type_assoc": [], 
                "rel_path": "output/f1",
                "children": [
                    {
                        "id": null, 
                        "name": "v#",
                        "asset_type_assoc": [], 
                        "rel_path": "output/f1/ver",
                        "children": []
                    }
                ]
            }
       ]
   }
]

Chamada de função:

callback_func(inp_json);

Saída conforme minha necessidade:

["output/f1/ver"]
Mohideen bin Mohammed
fonte
0

var test = {
    depth00: {
        depth10: 'string'
        , depth11: 11
        , depth12: {
            depth20:'string'
            , depth21:21
        }
        , depth13: [
            {
                depth22:'2201'
                , depth23:'2301'
            }
            , {
                depth22:'2202'
                , depth23:'2302'
            }
        ]
    }
    ,depth01: {
        depth10: 'string'
        , depth11: 11
        , depth12: {
            depth20:'string'
            , depth21:21
        }
        , depth13: [
            {
                depth22:'2201'
                , depth23:'2301'
            }
            , {
                depth22:'2202'
                , depth23:'2302'
            }
        ]
    }
    , depth02: 'string'
    , dpeth03: 3
};


function traverse(result, obj, preKey) {
    if(!obj) return [];
    if (typeof obj == 'object') {
        for(var key in obj) {
            traverse(result, obj[key], (preKey || '') + (preKey ? '[' +  key + ']' : key))
        }
    } else {
        result.push({
            key: (preKey || '')
            , val: obj
        });
    }
    return result;
}

document.getElementById('textarea').value = JSON.stringify(traverse([], test), null, 2);
<textarea style="width:100%;height:600px;" id="textarea"></textarea>

seung
fonte
chegou ao enviar o formulário enctype applicatioin / json
Seung
-1

A melhor solução para mim foi a seguinte:

simples e sem usar nenhuma estrutura

    var doSomethingForAll = function (arg) {
       if (arg != undefined && arg.length > 0) {
            arg.map(function (item) {
                  // do something for item
                  doSomethingForAll (item.subitem)
             });
        }
     }
Asqan
fonte
-1

Você pode obter todas as chaves / valores e preservar a hierarquia com este

// get keys of an object or array
function getkeys(z){
  var out=[]; 
  for(var i in z){out.push(i)};
  return out;
}

// print all inside an object
function allInternalObjs(data, name) {
  name = name || 'data';
  return getkeys(data).reduce(function(olist, k){
    var v = data[k];
    if(typeof v === 'object') { olist.push.apply(olist, allInternalObjs(v, name + '.' + k)); }
    else { olist.push(name + '.' + k + ' = ' + v); }
    return olist;
  }, []);
}

// run with this
allInternalObjs({'a':[{'b':'c'},{'d':{'e':5}}],'f':{'g':'h'}}, 'ob')

Esta é uma modificação em ( https://stackoverflow.com/a/25063574/1484447 )

Ricky
fonte
-1
             var localdata = [{''}]// Your json array
              for (var j = 0; j < localdata.length; j++) 
               {$(localdata).each(function(index,item)
                {
                 $('#tbl').append('<tr><td>' + item.FirstName +'</td></tr>);
                 }
RAJ KADAM
fonte
-1

Eu criei uma biblioteca para percorrer e editar objetos JS aninhados profundos. Confira a API aqui: https://github.com/dominik791

Você também pode brincar com a biblioteca interativamente usando o aplicativo demo: https://dominik791.github.io/obj-traverse-demo/

Exemplos de uso: você sempre deve ter um objeto raiz, que é o primeiro parâmetro de cada método:

var rootObj = {
  name: 'rootObject',
  children: [
    {
      'name': 'child1',
       children: [ ... ]
    },
    {
       'name': 'child2',
       children: [ ... ]
    }
  ]
};

O segundo parâmetro é sempre o nome da propriedade que contém objetos aninhados. No caso acima, seria'children' .

O terceiro parâmetro é um objeto que você usa para encontrar objetos que deseja localizar / modificar / excluir. Por exemplo, se você estiver procurando por objetos com um ID igual a 1, passará{ id: 1} como o terceiro parâmetro.

E você pode:

  1. findFirst(rootObj, 'children', { id: 1 }) para encontrar o primeiro objeto com id === 1
  2. findAll(rootObj, 'children', { id: 1 }) para encontrar todos os objetos com id === 1
  3. findAndDeleteFirst(rootObj, 'children', { id: 1 }) excluir o primeiro objeto correspondente
  4. findAndDeleteAll(rootObj, 'children', { id: 1 }) excluir todos os objetos correspondentes

replacementObj é usado como o último parâmetro em dois últimos métodos:

  1. findAndModifyFirst(rootObj, 'children', { id: 1 }, { id: 2, name: 'newObj'})para alterar o primeiro objeto encontrado com id === 1para o{ id: 2, name: 'newObj'}
  2. findAndModifyAll(rootObj, 'children', { id: 1 }, { id: 2, name: 'newObj'})para alterar todos os objetos com id === 1para o{ id: 2, name: 'newObj'}
dominik791
fonte