Recorre todos los nodos de un árbol de objetos JSON con JavaScript

148

Me gustaría atravesar un árbol de objetos JSON, pero no puedo encontrar ninguna biblioteca para eso. No parece difícil, pero se siente como reinventar la rueda.

En XML hay muchos tutoriales que muestran cómo atravesar un árbol XML con DOM :(

Patsy Issa
fuente
1
Hecho iterador IIFE github.com/eltomjan/ETEhomeTools/blob/master/HTM_HTA/ ... tiene predefinido (básico) DepthFirst & BreadthFirst siguiente y la capacidad de moverse dentro de la estructura JSON sin recurrencia.
Tom

Respuestas:

222

Si crees que jQuery es una exageración para una tarea tan primitiva, podrías hacer algo así:

//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
fuente
2
¿Por qué fund.apply (esto, ...)? ¿No debería ser func.apply (o, ...)?
Craig Celeste
44
@ParchedSquid No. Si observa los documentos de la API para apply (), el primer parámetro es el thisvalor de la función de destino, mientras que odebería ser el primer parámetro de la función. Sin embargo, establecerlo en this(que sería la traversefunción) es un poco extraño, pero no es como processusar la thisreferencia de todos modos. Bien podría haber sido nulo.
Thor84no
1
Para jshint en modo estricto, es posible que deba agregar más /*jshint validthis: true */arriba func.apply(this,[i,o[i]]);para evitar el error W040: Possible strict violation.causado por el uso dethis
Jasdeep Khalsa
44
@jasdeepkhalsa: Eso es cierto. Pero en el momento de escribir la respuesta, jshint ni siquiera se inició como un proyecto durante un año y medio.
TheHippo
1
@Vishal puede agregar un parámetro 3 a la traversefunción que rastrea la profundidad. Wenn llamando recursivamente agrega 1 al nivel actual.
TheHippo
75

Un objeto JSON es simplemente un objeto Javascript. En realidad, eso es lo que JSON significa: notación de objetos JavaScript. Por lo tanto, atravesaría un objeto JSON, sin embargo, elegiría "atravesar" un objeto Javascript en general.

En ES2017 harías:

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

Siempre puedes escribir una función para descender recursivamente al 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 debería ser un buen punto de partida. Recomiendo encarecidamente utilizar métodos javascript modernos para tales cosas, ya que hacen que escribir dicho código sea mucho más fácil.

Eli Courtwright
fuente
9
Evite atravesar (v) donde v == nulo, porque (typeof null == "objeto") === verdadero. function traverse(jsonObj) { if(jsonObj && typeof jsonObj == "object" ) { ...
Marcelo Amorim
44
Odio sonar pedante, pero creo que ya hay mucha confusión sobre esto, así que solo por claridad, digo lo siguiente. Los objetos JSON y JavaScript no son lo mismo. JSON se basa en el formato de objetos JavaScript, pero JSON es solo la notación ; Es una cadena de caracteres que representa un objeto. Todos los JSON se pueden "analizar" en un objeto JS, pero no todos los objetos JS se pueden "encadenar" en JSON. Por ejemplo, los objetos JS autorreferenciales no se pueden stringificar.
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
fuente
66
¿Podría explicar por qué es much better?
Demencia el
3
Si el método está destinado a hacer otra cosa que no sea el registro, debe verificar nulo, nulo sigue siendo un objeto.
wi1
3
@ wi1 De acuerdo con usted, podría verificar!!o[i] && typeof o[i] == 'object'
pilau
32

Hay una nueva biblioteca para atravesar datos JSON con JavaScript que admite muchos casos de uso diferentes.

https://npmjs.org/package/traverse

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

Funciona con todo tipo de objetos JavaScript. Incluso detecta ciclos.

También proporciona la ruta de cada nodo.

Benjamin Atkin
fuente
1
js-traverse también parece estar disponible a través de npm en node.js.
Ville
Si. Simplemente se llama transversal allí. ¡Y tienen una página web encantadora! Actualizando mi respuesta para incluirlo.
Benjamin Atkin
15

Depende de lo que quieras hacer. Aquí hay un ejemplo de atravesar un árbol de objetos JavaScript, imprimir claves y valores a medida que avanza:

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

Si está atravesando una cadena JSON real , puede usar una función de 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)
})

Al atravesar un 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
fuente
8

EDITAR : Todos los ejemplos a continuación en esta respuesta se han editado para incluir una nueva variable de ruta producida por el iterador según la solicitud de @ supersan . La variable de ruta es una matriz de cadenas donde cada cadena en la matriz representa cada clave a la que se accedió para obtener el valor iterado resultante del objeto fuente original. La variable de ruta se puede alimentar a la función / método get de lodash . O podría escribir su propia versión de lodash get que maneja solo matrices de esta manera:

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"]));

EDITAR : Esta respuesta editada resuelve recorridos en bucle infinito.

Detener molestos recorridos de objetos infinitos

Esta respuesta editada aún proporciona uno de los beneficios adicionales de mi respuesta original, que le permite usar la función de generador proporcionada para usar una interfaz iterativa más limpia y simple (piense en usar for ofbucles como en for(var a of b)donde bes iterable y aes un elemento del iterable). ) Al usar la función de generador, además de ser una API más simple, también ayuda con la reutilización del código al hacer que no tenga que repetir la lógica de iteración en todas partes donde desee iterar profundamente en las propiedades de un objeto y también hace posiblebreak salir de el bucle si desea detener la iteración antes.

Una cosa que noté que no se ha abordado y que no está en mi respuesta original es que debe tener cuidado al atravesar objetos arbitrarios (es decir, cualquier conjunto "aleatorio"), porque los objetos JavaScript pueden ser auto-referenciados. Esto crea la oportunidad de tener recorridos en bucle infinito. Sin embargo, los datos JSON no modificados no pueden ser auto-referenciados, por lo que si está utilizando este subconjunto particular de objetos JS, no tiene que preocuparse por los recorridos de bucle infinito y puede consultar mi respuesta original u otras respuestas. Aquí hay un ejemplo de un recorrido sin fin (tenga en cuenta que no es un código ejecutable, porque de lo contrario se bloquearía la pestaña del navegador).

También en el objeto generador en mi ejemplo editado, opté por usar en Object.keyslugar de for inque itera solo claves no prototipo en el objeto. Puede cambiar esto usted mismo si desea que se incluyan las claves prototipo. Vea mi sección de respuestas original a continuación para ambas implementaciones con Object.keysy for in.

Peor: esto hará un bucle infinito en objetos autorreferenciales:

//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 salvarse de esto, puede agregar un conjunto dentro de un cierre, de modo que cuando se llama a la función por primera vez, comience a construir una memoria de los objetos que ha visto y no continúe la iteración una vez que se encuentre con un objeto ya visto. El siguiente fragmento de código hace eso y, por lo tanto, maneja casos de bucle infinito.

Mejor: este no será un bucle infinito en objetos autorreferenciales:

//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);
}


Respuesta original

Para obtener una forma más nueva de hacerlo si no le importa descartar IE y admitir principalmente navegadores más actuales (consulte la tabla es6 de kangax para ver la compatibilidad). Puede usar generadores es2015 para esto. He actualizado la respuesta de @ TheHippo en consecuencia. Por supuesto, si realmente desea el soporte de IE, puede usar el transpilador JavaScript de babel .

//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);
}

Si solo desea propiedades propias enumerables (básicamente propiedades de cadena no prototipo), puede cambiarlo para iterar usando Object.keysy un for...ofbucle en su lugar:

//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);
}

Juan
fuente
¡Gran respuesta! ¿Es posible devolver rutas como abc, abcd, etc. para cada clave que se está atravesando?
supersan
1
@supersan puedes ver mis fragmentos de código actualizados. Agregué una variable de ruta a cada uno que es una matriz de cadenas. Las cadenas en la matriz representan cada clave a la que se accedió para obtener el valor iterado resultante del objeto fuente original.
John
4

Quería usar la solución perfecta de @TheHippo en una función anónima, sin el uso de procesos y funciones de activación. Lo siguiente funcionó para mí, compartiendo para programadores novatos como yo.

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

La mayoría de los motores de Javascript no optimizan la recursividad de cola (esto podría no ser un problema si su JSON no está profundamente anidado), pero por lo general me equivoco con precaución y en su lugar hago iteraciones, por ejemplo

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)
Max
fuente
0

Mi guión:

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

Entrada JSON:

[
    {
        "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": []
                    }
                ]
            }
       ]
   }
]

Llamada de función:

callback_func(inp_json);

Salida según mi necesidad:

["output/f1/ver"]
Mohideen bin Mohammed
fuente
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
fuente
lo hizo para enviar formulario enctype solicitud / json
seung
-1

La mejor solución para mí fue la siguiente:

simple y sin usar ningún marco

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

Puede obtener todas las claves / valores y preservar la jerarquía con esto

// 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 es una modificación en ( https://stackoverflow.com/a/25063574/1484447 )

Ricky
fuente
-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
fuente
-1

He creado una biblioteca para atravesar y editar objetos JS profundamente anidados. Echa un vistazo a la API aquí: https://github.com/dominik791

También puedes jugar con la biblioteca de manera interactiva usando la aplicación de demostración: https://dominik791.github.io/obj-traverse-demo/

Ejemplos de uso: siempre debe tener un objeto raíz, que es el primer parámetro de cada método:

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

El segundo parámetro es siempre el nombre de la propiedad que contiene los objetos anidados. En el caso anterior sería 'children'.

El tercer parámetro es un objeto que utiliza para buscar objetos / objetos que desea encontrar / modificar / eliminar. Por ejemplo, si está buscando un objeto con una identificación igual a 1, pasará { id: 1}como tercer parámetro.

Y tu puedes:

  1. findFirst(rootObj, 'children', { id: 1 }) para encontrar el primer objeto con id === 1
  2. findAll(rootObj, 'children', { id: 1 }) para encontrar todos los objetos con id === 1
  3. findAndDeleteFirst(rootObj, 'children', { id: 1 }) para eliminar el primer objeto coincidente
  4. findAndDeleteAll(rootObj, 'children', { id: 1 }) para eliminar todos los objetos coincidentes

replacementObj se usa como el último parámetro en dos últimos métodos:

  1. findAndModifyFirst(rootObj, 'children', { id: 1 }, { id: 2, name: 'newObj'})para cambiar el primer objeto encontrado con id === 1el{ id: 2, name: 'newObj'}
  2. findAndModifyAll(rootObj, 'children', { id: 1 }, { id: 2, name: 'newObj'})para cambiar todos los objetos con id === 1el{ id: 2, name: 'newObj'}
dominik791
fuente