JSON récursif.

J'essaie d'apprendre la récursivité en version Javascript, alors j'ai pensé que je réécrirais la fonction native JSON.stringify utilisant la récurrence comme un défi pour moi-même. J'ai presque mis mon code au travail:

 var my_stringify = function(obj){ value = obj[ Object.keys(obj)[0] ]; index = Object.keys(obj)[0]; delete obj[ Object.keys(obj)[0] ]; // The value is just a simple string, not a nested object if (typeof value === 'string'){ if (Object.keys(obj).length !== 0){ // Continue recursion .. return '"' + index + '":"' + value + '",' + my_stringify(obj); } // This would be the base case with a string at the end. Stop recursion. return '"' + index + '":"' + value + '"}'; } // The value is actually a nested object else{ if (Object.keys(obj).length !== 0){ // Continue recursion .. return '"' + index + '":{' + my_stringify(value) + ',' + my_stringify(obj); } // This is the base case with a nested object at the end. Stringify it and end recursion. return '"' + index + '":{' + my_stringify(value) + '}'; } } 

Sauf pour le fait que le premier { dans ma réponse manque, et je ne peux pas comprendre comment réparer ce bug.

Par exemple my_stringify({foo: 'bar'}) renvoie "foo":"bar"} au lieu de {"foo":"bar"} .

Aussi, je suis conscient que je détruit complètement l'objet original, est-il possible d'envoyer à récursion une version réduite de l'objet d'origine sans supprimer quoi que ce soit (quelque chose comme obj.slice(1) )?

Tout conseil sera grandement apprécié !

Nouvelle réponse à une ancienne question

Il y a des réponses douloureusement mauvaises qui échouent même dans les exemples les plus simples. Cette réponse vise à répondre à la question de manière exhaustive et à démontrer comment une approche comme celle-ci varie même si vous manipulez une grande variété de types de données et …

Cas d'angle

Cette fonction effectue une analyse de cas simple sur une propriété de constructor de données non nulles et encode en conséquence. Il parvient à couvrir un grand nombre de cas d'angle dont il est peu probable que vous considériez, comme

  • JSON.stringify(undefined) retourne undefined
  • JSON.stringify(null) renvoie 'null'
  • JSON.stringify(true) renvoie 'true'
  • JSON.stringify([1,2,undefined,4]) renvoie '[1,2,null,4]'
  • JSON.stringify({a: undefined, b: 2}) renvoie '{ "b": 2 }'
  • JSON.stringify({[undefined]: 1}) retourne '{ "undefined": 1 }'
  • JSON.stringify({a: /foo/}) retourne { "a": {} }

Donc, pour vérifier que notre fonction stringifyJSON fonctionne correctement, je ne vais pas vérifier la sortie directement. Au lieu de cela, je vais écrire une petite méthode d' test qui garantit que JSON.parse de notre JSON encodé renvoie réellement notre valeur de saisie d'origine

 // we really only care that JSON.parse can work with our result // the output value should match the input value // if it doesn't, we did something wrong in our stringifier const test = data => { return console.log(JSON.parse(stringifyJSON(data))) } test([1,2,3]) // should return [1,2,3] test({a:[1,2,3]}) // should return {a:[1,2,3]} 

Avertissement: il devrait être évident que le code que je suis sur le point de partager ne doit pas être utilisé comme un remplacement réel pour JSON.stringify – il y a d'innombrables cas d'angle que nous n'avons probablement pas JSON.stringify . Au lieu de cela, ce code est partagé pour fournir une démonstration de la façon dont nous pourrions faire une telle tâche. Des cas d'angle supplémentaires pourraient facilement être ajoutés à cette fonction.


Démo disponible

Sans plus tarder, voici stringifyJSON dans une démonstration stringifyJSON qui vérifie une excellente compatibilité pour plusieurs cas communs

 const stringifyJSON = data => { if (data === undefined) return undefined else if (data === null) return 'null' else if (data.constructor === String) return '"' + data.replace(/"/g, '\\"') + '"' else if (data.constructor === Number) return String(data) else if (data.constructor === Boolean) return data ? 'true' : 'false' else if (data.constructor === Array) return '[ ' + data.reduce((acc, v) => { if (v === undefined) return [...acc, 'null'] else return [...acc, stringifyJSON(v)] }, []).join(', ') + ' ]' else if (data.constructor === Object) return '{ ' + Object.keys(data).reduce((acc, k) => { if (data[k] === undefined) return acc else return [...acc, stringifyJSON(k) + ':' + stringifyJSON(data[k])] }, []).join(', ') + ' }' else return '{}' } // round-trip test and log to console const test = data => { return console.log(JSON.parse(stringifyJSON(data))) } test(null) // null test('he said "hello"') // 'he said "hello"' test(5) // 5 test([1,2,true,false]) // [ 1, 2, true, false ] test({a:1, b:2}) // { a: 1, b: 2 } test([{a:1},{b:2},{c:3}]) // [ { a: 1 }, { b: 2 }, { c: 3 } ] test({a:[1,2,3], c:[4,5,6]}) // { a: [ 1, 2, 3 ], c: [ 4, 5, 6 ] } test({a:undefined, b:2}) // { b: 2 } test({[undefined]: 1}) // { undefined: 1 } test([[["test","mike",4,["jake"]],3,4]]) // [ [ [ 'test', 'mike', 4, [ 'jake' ] ], 3, 4 ] ] 

Vous devez afficher la récursivité comme une exploration plus profonde dans l'objet sans altérer l'objet. Il semble que vous essayez d'utiliser la récursivité pour aller de côté à l'intérieur d'un objet.

J'ai écrit une version de stringify qui gère l'objet de base (pas de tableaux ou de fonctions).

Voici le violon

Voici le code:

 var my_stringify2 = function (obj) { var objKeys = Object.keys(obj); var keyValueArray = new Array(); for (var i = 0; i < objKeys.length; i++) { var keyValueString = '"' + objKeys[i] + '":'; var objValue = obj[objKeys[i]]; keyValueString = (typeof objValue == "string") ? keyValueString = keyValueString + '"' + objValue + '"' : keyValueString = keyValueString + my_stringify2(objValue); keyValueArray.push(keyValueString); } return "{" + keyValueArray.join(",") + "}"; } 

Vous voulez que la récursivité vous fasse la plupart du travail, et que vous devriez seulement gérer les conditions de base (ce que vous aviez déjà). Dans ma fonction, les deux conditions acceptables sont la chaîne et l'objet.

Une chaîne est traitée sur place, et un objet passe dans la fonction de manière récursive.

C'est la clé. Vous passiez le même objet à la fonction à plusieurs reprises, en supprimant les éléments manipulés jusqu'à ce que vous arriviez à un point où l'objet est complètement disparu.

Ce que j'ai fait à la place, c'est de passer la valeur de cette propriété particulière s'il s'agissait d'un objet. Si c'est une chaîne, ajoutez-la simplement à la chaîne et déplacez-vous.

Jetez un coup d'œil au code et faites-le moi savoir si vous avez des questions. Notez que l'objet dans lequel je passe a un objet imbriqué.

 my_stringify2({ foo: 'bar', bar: 'foo', foobar: { foo: 'bar', bar: 'foo' } }); 

Et le résultat est bon json

 {"foo":"bar","bar":"foo","foobar":{"foo":"bar","bar":"foo"}} 

Si vous cherchez à éviter complètement une boucle for, vous pouvez faire ce qui suit

Jsfiddle

Dans celui-ci, vous passez l'objet comme une normale, mais récursivement, vous passez un tableau de clés, en supprimant un élément du tableau de clés pour chaque propriété.

Un peu plus compliqué, alors j'ai ajouté des commentaires

 var my_stringify2 = function (obj, objKeys) { var str = ""; // keys haven't been loaded, either first pass, or processing a value of type object if (objKeys == undefined) { objKeys = Object.keys(obj); str = "{" } else { // if keys array exists and is empty, no more properties to evaluate, return the end bracket if (objKeys.length == 0) { return "}"; // array exists and isn't empty, that means it's a property and not the first property, add a comma } else { str = ","; } } // add the property name str += '"' + objKeys[0] + '":'; // get the value var objValue = obj[objKeys[0]]; // if the value type is string, add the string, if it's an object, call this function again, but leave the objKeys undefined str += (typeof objValue == "string") ? '"' + objValue + '"' : my_stringify2(objValue); // remove the first element fromt the keys array objKeys.splice(0,1); //call the function for the next property return str + my_stringify2(obj, objKeys); } 

Sur le plan fondamental, vous êtes en train de caler en coupant la première propriété, en la calandant et en récupérant le reste de l'objet. IMHO ce n'est pas le chemin à parcourir, la seule raison de reculer est quand il y a un objet imbriqué, sinon vous devriez simplement itérer à travers les propriétés. Comme vous l'avez fait, vous avez rendu beaucoup plus difficile de dire si vous êtes au début de l'objet et devrait renvoyer ce manque { avec votre chaîne.

En semi-pseudo-code (vous laisser du travail à faire vous-même), vous voulez quelque chose comme ça:

 var my_stringify = function(obj) { // check first for null / undefined / etc and return var myJSON = "{"; // iterate through all the properties of the object for (var p in obj) { if (obj.hasOwnProperty(p)) { // check to see if this property is a string, number, etc if (//...) { myJSON += // the JSON representation of this value using p and obj[p] } if (// test for nested object) { myJSON += my_stringify(obj[p]); // this is recursion! } if (// test for arrays) { // arrays also need special handling and note that they might // include objects or other arrays - more chances for recursion! } // note: functions should be ignored, they aren't included in JSON } } return myJSON + "}"; } 

Je ne suis pas d'accord avec l'affirmation de @ Bergi selon laquelle une récurrence régulière ne convient pas pour cela. Comme je l'ai dit dans mon commentaire, vous pouvez éviter l'utilisation d'une boucle for en passant l'index comme argument de la fonction. Ceci est une technique très courante et vous empêche d'avoir besoin de copier ou de modifier la structure de données.

Voici ma coup d'oeil à une telle mise en œuvre. Comme vous pouvez le voir, c'est vraiment simple (et à ma propre surprise, ça marche!):

 function jsonify(obj, idx) { var json, objStr = toString.call(obj); // Handle strings if(objStr == '[object String]') { return '"' + obj + '"' } idx = idx || 0 // Handle arrays if(objStr == '[object Array]') { if(idx >= obj.length) { // The code below ensures we'll never go past the end of the array, // so we can assume this is an empty array return "[]" } // JSONify the value at idx json = jsonify( obj[idx] ) if(idx < obj.length - 1) { // There are items left in the array, so increment the index and // JSONify the rest json = json + "," + jsonify( obj, idx + 1 ) } // If this is the first item in the array, wrap the result in brackets if(idx === 0) { return "[" + json + "]" } return json } // Handle objects if(obj === Object(obj)) { var keys = Object.keys(obj) var key = keys[idx] // JSONify the key and value json = '"' + key + '":' + jsonify( obj[key] ) if(idx < keys.length - 1) { // There are more keys, so increment the index and JSONify the rest return json + "," + jsonify( obj, idx + 1 ) } // If this is the first key, wrap the result in curly braces if(idx === 0) { return "{" + json + "}" } return json } return obj.toString() // Naively handle everything else } var items = [ 9, "nine", { "key": [], "key2": { "subkey": 3.333 } } ] console.log("OUTPUT", jsonify(items)) // => OUTPUT [9,"nine","key":[],"key2":{"subkey":3.333}] 

Il y a plusieurs façons pour lesquelles cela pourrait être resserré (et je suis sûr qu'il y a des bugs aussi), mais vous avez l'idée.

On m'a demandé cela dans une question d'entrevue et c'est ce que j'ai proposé. Une approche récursive compréhensible:

 ​function stringify(input) { var arrVals = []; Object.keys(input).forEach(function(keyName) { let val = input[keyName]; if (typeof val !== 'undefined' && typeof val !== 'function') { arrVals.push(getQuotedString(keyName) + ":" + getString(val)); } }); return '{' + arrVals.join(',') + '}'; } function getString(val) { switch (typeof val) { case 'string': return getQuotedString(val); break; case 'number': case 'boolean': return val; break; case 'object': if (val === null) { return "null"; } if (Array.isArray(val)) { let arrString = [] for (let i = 0; i < val.length; i++) { arrString.push(getString(val[i])); } return "[" + arrString.join(',') + "]"; } return stringify(val); break; } } function getQuotedString(str) { return '"' + str + '"'; }  ​function stringify(input) { var arrVals = []; Object.keys(input).forEach(function(keyName) { let val = input[keyName]; if (typeof val !== 'undefined' && typeof val !== 'function') { arrVals.push(getQuotedString(keyName) + ":" + getString(val)); } }); return '{' + arrVals.join(',') + '}'; } function getString(val) { switch (typeof val) { case 'string': return getQuotedString(val); break; case 'number': case 'boolean': return val; break; case 'object': if (val === null) { return "null"; } if (Array.isArray(val)) { let arrString = [] for (let i = 0; i < val.length; i++) { arrString.push(getString(val[i])); } return "[" + arrString.join(',') + "]"; } return stringify(val); break; } } function getQuotedString(str) { return '"' + str + '"'; }