Traversez tous les nœuds d'une arborescence d'objets JSON avec JavaScript

148

J'aimerais parcourir une arborescence d'objets JSON, mais je ne trouve aucune bibliothèque pour cela. Cela ne semble pas difficile mais c'est comme réinventer la roue.

En XML, il y a tellement de tutoriels montrant comment parcourir une arborescence XML avec DOM :(

Patsy Issa
la source
1
Made iterator IIFE github.com/eltomjan/ETEhomeTools/blob/master/HTM_HTA/… il a prédéfini (de base) DepthFirst & BreadthFirst ensuite et la possibilité de se déplacer dans la structure JSON sans récursivité.
Tom

Réponses:

222

Si vous pensez que jQuery est un peu exagéré pour une tâche aussi primitive, vous pouvez faire quelque chose comme ça:

//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
la source
2
Pourquoi fund.apply (this, ...)? Ne devrait-il pas être func.apply (o, ...)?
Craig Celeste
4
@ParchedSquid Non. Si vous regardez la documentation de l' API pour apply (), le premier paramètre est la thisvaleur de la fonction cible, alors qu'il odevrait être le premier paramètre de la fonction. Le définir sur this(ce qui serait la traversefonction) est cependant un peu étrange, mais ce n'est pas comme si vous processutilisiez la thisréférence de toute façon. Cela aurait tout aussi bien pu être nul.
Thor84no
1
Pour jshint en mode strict, vous devrez peut-être ajouter /*jshint validthis: true */ci-dessus func.apply(this,[i,o[i]]);pour éviter l'erreur W040: Possible strict violation.causée par l'utilisation dethis
Jasdeep Khalsa
4
@jasdeepkhalsa: C'est vrai. Mais au moment de la rédaction de la réponse, jshint n'était même pas un projet lancé depuis un an et demi.
TheHippo
1
@Vishal vous pouvez ajouter un paramètre 3 à la traversefonction qui suit la profondeur. Wenn appelant récursivement ajoute 1 au niveau actuel.
TheHippo
75

Un objet JSON est simplement un objet Javascript. C'est en fait ce que signifie JSON: la notation d'objets JavaScript. Vous traverseriez donc un objet JSON comme vous choisiriez de "traverser" un objet Javascript en général.

Dans ES2017, vous feriez:

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

Vous pouvez toujours écrire une fonction pour descendre récursivement dans l'objet:

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

Cela devrait être un bon point de départ. Je recommande vivement d'utiliser des méthodes javascript modernes pour de telles choses, car elles facilitent beaucoup l'écriture de ce code.

Eli Courtwright
la source
9
Évitez de traverser (v) où v == null, car (typeof null == "object") === true. function traverse(jsonObj) { if(jsonObj && typeof jsonObj == "object" ) { ...
Marcelo Amorim
4
Je déteste avoir l'air pédant, mais je pense qu'il y a déjà beaucoup de confusion à ce sujet, donc juste pour des raisons de clarté, je dis ce qui suit. Les objets JSON et JavaScript ne sont pas la même chose. JSON est basé sur le formatage des objets JavaScript, mais JSON n'est que la notation ; c'est une chaîne de caractères représentant un objet. Tous les JSON peuvent être "analysés" dans un objet JS, mais tous les objets JS ne peuvent pas être "stringifiés" en JSON. Par exemple, les objets JS auto-référentiels ne peuvent pas être stringifiés.
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
la source
6
Pouvez-vous expliquer pourquoi much better?
Dementic
3
Si la méthode est destinée à faire autre chose que journaliser, vous devez vérifier la valeur null, null est toujours un objet.
wi1
3
@ wi1 D'accord avec vous, pourrait vérifier!!o[i] && typeof o[i] == 'object'
pilau
32

Il existe une nouvelle bibliothèque pour parcourir les données JSON avec JavaScript qui prend en charge de nombreux cas d'utilisation différents.

https://npmjs.org/package/traverse

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

Il fonctionne avec toutes sortes d'objets JavaScript. Il détecte même les cycles.

Il fournit également le chemin de chaque nœud.

Benjamin Atkin
la source
1
js-traverse semble également être disponible via npm dans node.js.
Ville
Oui. C'est juste appelé traversée là-bas. Et ils ont une belle page Web! Mettre à jour ma réponse pour l'inclure.
Benjamin Atkin
15

Tout dépends de ce que tu veux faire. Voici un exemple de parcours d'une arborescence d'objets JavaScript, d'impression de clés et de valeurs au fur et à mesure:

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
la source
9

Si vous parcourez une chaîne JSON réelle, vous pouvez utiliser une fonction 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)
})

Lors de la traversée d'un objet:

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
la source
8

EDIT : Tous les exemples ci-dessous dans cette réponse ont été modifiés pour inclure une nouvelle variable de chemin fournie par l'itérateur conformément à la demande de @ supersan . La variable path est un tableau de chaînes où chaque chaîne du tableau représente chaque clé à laquelle on a accédé pour accéder à la valeur itérée résultante à partir de l'objet source d'origine. La variable path peut être introduite dans la fonction / méthode get de lodash . Ou vous pouvez écrire votre propre version de get de lodash qui ne gère que les tableaux comme ceci:

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 : Cette réponse modifiée résout des traversées en boucle infinies.

Arrêter les traversées d'objets infinis

Cette réponse modifiée fournit toujours l'un des avantages supplémentaires de ma réponse originale qui vous permet d'utiliser la fonction de générateur fournie afin d'utiliser une interface itérable plus propre et simple (pensez à utiliser des for ofboucles comme dans for(var a of b)best un itérable et aest un élément de l'itérable ). En utilisant la fonction de générateur en plus d'être une API plus simple, cela aide également à la réutilisation du code en évitant d'avoir à répéter la logique d'itération partout où vous souhaitez itérer en profondeur sur les propriétés d'un objet et cela permet également debreak sortir de la boucle si vous souhaitez arrêter l'itération plus tôt.

Une chose que je remarque et qui n'a pas été abordée et qui n'est pas dans ma réponse originale est que vous devez être prudent en parcourant des objets arbitraires (c'est-à-dire tout ensemble "aléatoire"), car les objets JavaScript peuvent être auto-référencés. Cela crée la possibilité d'avoir des traversées en boucle infinies. Les données JSON non modifiées ne peuvent cependant pas être auto-référencées, donc si vous utilisez ce sous-ensemble particulier d'objets JS, vous n'avez pas à vous soucier des traversées en boucle infinies et vous pouvez vous référer à ma réponse originale ou à d'autres réponses. Voici un exemple de parcours sans fin (notez que ce n'est pas un morceau de code exécutable, car sinon, il planterait l'onglet de votre navigateur).

Également dans l'objet générateur de mon exemple édité, j'ai choisi d'utiliser à la Object.keysplace de for inlaquelle n'itère que des clés non prototypes sur l'objet. Vous pouvez l'échanger vous-même si vous souhaitez que les clés prototypes soient incluses. Voir ma section de réponse originale ci-dessous pour les deux implémentations avec Object.keyset for in.

Pire - Cela fera une boucle infinie sur les objets auto-référentiels:

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

Pour vous sauver de cela, vous pouvez ajouter un ensemble dans une fermeture, de sorte que lorsque la fonction est appelée pour la première fois, elle commence à créer une mémoire des objets qu'elle a vus et ne continue pas l'itération une fois qu'elle rencontre un objet déjà vu. L'extrait de code ci-dessous fait cela et gère donc des cas de boucle infinis.

Mieux - Ce ne sera pas une boucle infinie sur les objets auto-référentiels:

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


Réponse originale

Pour une nouvelle façon de le faire si cela ne vous dérange pas d'abandonner IE et de prendre en charge principalement les navigateurs plus récents (consultez le tableau es6 de kangax pour la compatibilité). Vous pouvez utiliser les générateurs es2015 pour cela. J'ai mis à jour la réponse de @ TheHippo en conséquence. Bien sûr, si vous voulez vraiment le support IE, vous pouvez utiliser le transpilateur JavaScript 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 vous ne voulez que des propriétés énumérables propres (essentiellement des propriétés de chaîne non prototypes), vous pouvez le modifier pour itérer en utilisant Object.keyset une for...ofboucle à la place:

//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
la source
Très bonne réponse! Est-il possible de renvoyer des chemins comme abc, abcd, etc. pour chaque clé parcourue?
supersan
1
@supersan, vous pouvez voir mes extraits de code mis à jour. J'ai ajouté une variable de chemin à chacun qui est un tableau de chaînes. Les chaînes du tableau représentent chaque clé à laquelle on a accédé pour accéder à la valeur itérée résultante à partir de l'objet source d'origine.
John
4

Je voulais utiliser la solution parfaite de @TheHippo dans une fonction anonyme, sans utilisation de fonctions de processus et de déclenchement. Ce qui suit a fonctionné pour moi, le partage pour les programmeurs novices comme moi.

(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
la source
2

La plupart des moteurs Javascript n'optimisent pas la récursivité de la queue (cela peut ne pas être un problème si votre JSON n'est pas profondément imbriqué), mais je me trompe généralement par prudence et je fais plutôt des itérations, par exemple

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
la source
0

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

Entrée 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": []
                    }
                ]
            }
       ]
   }
]

Appel de fonction:

callback_func(inp_json);

Sortie selon mon besoin:

["output/f1/ver"]
Mohideen bin Mohammed
la source
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
la source
fait à soumettre sous forme enctype applicatioin / JSON
seung
-1

La meilleure solution pour moi était la suivante:

simple et sans utiliser de framework

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

Vous pouvez obtenir toutes les clés / valeurs et préserver la hiérarchie avec ceci

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

Ceci est une modification sur ( https://stackoverflow.com/a/25063574/1484447 )

Ricky
la source
-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
la source
-1

J'ai créé une bibliothèque pour parcourir et modifier des objets JS imbriqués en profondeur. Découvrez l'API ici: https://github.com/dominik791

Vous pouvez également jouer avec la bibliothèque de manière interactive en utilisant l'application de démonstration: https://dominik791.github.io/obj-traverse-demo/

Exemples d'utilisation: Vous devez toujours avoir un objet racine qui est le premier paramètre de chaque méthode:

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

Le deuxième paramètre est toujours le nom de la propriété qui contient les objets imbriqués. Dans le cas ci-dessus, ce serait 'children'.

Le troisième paramètre est un objet que vous utilisez pour rechercher les objets / objets que vous souhaitez rechercher / modifier / supprimer. Par exemple, si vous recherchez un objet avec un id égal à 1, vous le passerez { id: 1}comme troisième paramètre.

Et tu peux:

  1. findFirst(rootObj, 'children', { id: 1 }) pour trouver le premier objet avec id === 1
  2. findAll(rootObj, 'children', { id: 1 }) pour trouver tous les objets avec id === 1
  3. findAndDeleteFirst(rootObj, 'children', { id: 1 }) pour supprimer le premier objet correspondant
  4. findAndDeleteAll(rootObj, 'children', { id: 1 }) pour supprimer tous les objets correspondants

replacementObj est utilisé comme dernier paramètre dans deux dernières méthodes:

  1. findAndModifyFirst(rootObj, 'children', { id: 1 }, { id: 2, name: 'newObj'})pour changer le premier objet trouvé avec id === 1le{ id: 2, name: 'newObj'}
  2. findAndModifyAll(rootObj, 'children', { id: 1 }, { id: 2, name: 'newObj'})pour changer tous les objets avec id === 1le{ id: 2, name: 'newObj'}
dominik791
la source