183 votes

Traverser tous les nœuds d'un arbre d'objets JSON avec JavaScript

J'aimerais parcourir un arbre d'objets JSON, mais je ne trouve aucune bibliothèque pour cela. Cela ne semble pas difficile mais j'ai l'impression de réinventer la roue.

Dans XML, il y a tellement de tutoriels montrant comment traverser un arbre XML avec DOM :(

249voto

TheHippo Points 11900

Si vous pensez que jQuery est un peu surenchère pour une tâche aussi primitive, vous pourriez 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);

94voto

Eli Courtwright Points 53071

Un objet JSON est tout simplement un objet Javascript. C'est en fait la signification de JSON : JavaScript Object Notation. Vous pouvez donc parcourir un objet JSON de la même manière que vous choisissez de "parcourir" un objet JavaScript en général.

Dans l'ES2017, vous le 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 les méthodes modernes de javascript pour ce genre de choses, car elles rendent l'écriture de ce code beaucoup plus facile.

42voto

tejas Points 111
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]);
        }
    }
}

34voto

bat Points 5380

Il existe une nouvelle bibliothèque permettant de parcourir des 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.

22voto

John Points 4028

Réponse originale simplifiée

Il existe une nouvelle façon de procéder si cela ne vous dérange pas d'abandonner IE et de prendre en charge principalement les navigateurs plus récents (voir la table es6 de kangax pour la compatibilité). Vous pouvez utiliser es2015 générateurs pour ça. J'ai mis à jour la réponse de @TheHippo en conséquence. Bien sûr, si vous voulez vraiment le support d'IE, vous pouvez utiliser la fonction babel Transpilateur JavaScript.

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

// Traverse usage:
//that's all... no magic, no bloated framework
for(var [key, value, path, parent] of traverse({ 
    foo:"bar",
    arr:[1,2,3],
    subo: {
        foo2:"bar2"
    }
})) {
  // do something here with each key and value
  console.log(key, value, path, parent);
}

Si vous voulez seulement posséder des propriétés énumérables (en fait, des propriétés de la chaîne de non-prototypes), vous pouvez le changer pour itérer en utilisant Object.keys et un for...of à la place :

function* traverse(o,path=[]) {
    for (var i of Object.keys(o)) {
        const itemPath = path.concat(i);
        yield [i,o[i],itemPath,o];
        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, parent] of traverse({ 
    foo:"bar",
    arr:[1,2,3],
    subo: {
        foo2:"bar2"
    }
})) {
  // do something here with each key and value
  console.log(key, value, path, parent);
}

EDITAR : Cette réponse éditée résout les traversées en boucle infinies.

Arrêter les trajets d'objets infinis gênants

Cette réponse modifiée offre toujours l'un des avantages supplémentaires de ma réponse originale, qui vous permet d'utiliser le système d'information fourni. fonction de générateur afin d'utiliser une méthode plus propre et plus simple interface itérable (pensez à utiliser for of boucles comme dans for(var a of b) donde b est une itérable et a est un élément de l'itérable). L'utilisation de la fonction de génération, en plus d'être une API plus simple, contribue également à la réutilisation du code en évitant de devoir répéter la logique d'itération chaque fois que l'on veut itérer profondément sur les propriétés d'un objet. break hors de la boucle si vous souhaitez arrêter l'itération plus tôt.

J'ai remarqué une chose qui n'a pas été abordée et qui ne figure pas dans ma réponse initiale : vous devez être prudent lorsque vous traversez des objets arbitraires (c'est-à-dire tout ensemble "aléatoire" d'objets), car les objets JavaScript peuvent être auto-référencés. Cela crée la possibilité d'avoir des traversées en boucle infinie. Cependant, les données JSON non modifiées ne peuvent 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 traversée sans fin (notez qu'il ne s'agit pas d'un morceau de code exécutable, car sinon il ferait planter l'onglet de votre navigateur).

De même, dans l'objet générateur de mon exemple modifié, j'ai choisi d'utiliser Object.keys au lieu de for in qui n'itère que sur les clés non-prototype de l'objet. Vous pouvez changer cela vous-même si vous voulez que les clés prototypes soient incluses. Voir la section de ma réponse originale ci-dessous pour les deux implémentations avec Object.keys y for in .

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

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

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

// this self-referential property assignment is the only real logical difference 
// from the above original example which ends up making this naive traversal 
// non-terminating (i.e. it makes it infinite loop)
o.o = o;

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

Pour éviter 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 à construire 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. C'est ce que fait l'extrait de code ci-dessous, qui permet de gérer les cas de boucles infinies.

Mieux - Cela ne fera pas de boucle infinie sur les objets autoréférentiels :

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, o]; 
      if (o[i] !== null && typeof(o[i])=="object") {
        //going one step down in the object tree!!
        yield* innerTraversal(o[i], itemPath);
      }
    }
  }
  yield* innerTraversal(o);
}

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

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

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

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

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

Vous pourriez également créer une fonction de réglage comme suit :

function set (object, path, value) {
    const obj = path.slice(0,-1).reduce((obj, pathItem) => obj ? obj[pathItem] : undefined, object)
    if(obj && obj[path[path.length - 1]]) {
        obj[path[path.length - 1]] = value;
    }
    return object;
}

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

EDIT Sep. 2020 : J'ai ajouté un parent pour un accès plus rapide à l'objet précédent. Cela pourrait vous permettre de construire plus rapidement un traverseur inverse. Vous pouvez aussi toujours modifier l'algorithme de traversée pour faire une recherche en largeur au lieu d'une recherche en profondeur, ce qui est probablement plus prévisible. une version TypeScript avec la recherche en largeur (Breadth First Search) . Comme il s'agit d'une question sur JavaScript, je vais mettre la version JS ici :

var TraverseFilter;
(function (TraverseFilter) {
    /** prevents the children from being iterated. */
    TraverseFilter["reject"] = "reject";
})(TraverseFilter || (TraverseFilter = {}));
function* traverse(o) {
    const memory = new Set();
    function* innerTraversal(root) {
        const queue = [];
        queue.push([root, []]);
        while (queue.length > 0) {
            const [o, path] = queue.shift();
            if (memory.has(o)) {
                // we've seen this object before don't iterate it
                continue;
            }
            // add the new object to our memory.
            memory.add(o);
            for (var i of Object.keys(o)) {
                const item = o[i];
                const itemPath = path.concat([i]);
                const filter = yield [i, item, itemPath, o];
                if (filter === TraverseFilter.reject)
                    continue;
                if (item !== null && typeof item === "object") {
                    //going one step down in the object tree!!
                    queue.push([item, itemPath]);
                }
            }
        }
    }
    yield* innerTraversal(o);
}
//your object
var o = {
    foo: "bar",
    arr: [1, 2, 3],
    subo: {
        foo2: "bar2"
    }
};
/// this self-referential property assignment is the only real logical difference
// from the above original example which makes more naive traversals
// non-terminating (i.e. it makes it infinite loop)
o.o = o;
//that's all... no magic, no bloated framework
for (const [key, value, path, parent] of traverse(o)) {
    // do something here with each key and value
    console.log(key, value, path, parent);
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X