Accès à des objets JavaScript imbriqués avec une clé de chaîne

J'ai une structure de données comme celle-ci:

var someObject = { 'part1' : { 'name': 'Part 1', 'size': '20', 'qty' : '50' }, 'part2' : { 'name': 'Part 2', 'size': '15', 'qty' : '60' }, 'part3' : [ { 'name': 'Part 3A', 'size': '10', 'qty' : '20' }, { 'name': 'Part 3B', 'size': '5', 'qty' : '20' }, { 'name': 'Part 3C', 'size': '7.5', 'qty' : '20' } ] }; 

Et j'aimerais avoir accès aux données en utilisant cette variable:

 var part1name = "part1.name"; var part2quantity = "part2.qty"; var part3name1 = "part3[0].name"; 

Part1name doit être rempli avec la valeur de someObject.part1.name , qui est "Part 1". Même chose avec la part2quantity qui a rempli 60.

Est-il possible d'y parvenir avec soit javascript pur ou JQuery?

Je viens de faire cela en fonction d'un code similaire que j'avais déjà, il semble fonctionner:

 Object.byString = function(o, s) { s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties s = s.replace(/^\./, ''); // strip a leading dot var a = s.split('.'); for (var i = 0, n = a.length; i < n; ++i) { var k = a[i]; if (k in o) { o = o[k]; } else { return; } } return o; } 

Usage::

 Object.byString(someObj, 'part3[0].name'); 

Voir une démonstration de travail à http://jsfiddle.net/alnitak/hEsys/

EDIT que certains ont remarqué que ce code provoquera une erreur si une chaîne est passée où les index les plus à gauche ne correspondent pas à une entrée correctement imbriquée dans l'objet. C'est une préoccupation valable, mais IMHO répond le mieux avec un bloc try / catch lors de l'appel, plutôt que d'avoir cette fonction renvoyer silencieusement undefined pour un index invalide.

C'est la solution que j'utilise:

 Object.resolve = function(path, obj) { return path.split('.').reduce(function(prev, curr) { return prev ? prev[curr] : undefined }, obj || self) } 

Exemple d'utilisation:

 Object.resolve("document.body.style.width") // or Object.resolve("style.width", document.body) // or even use array indexes // (someObject has been defined in the question) Object.resolve("part3.0.size", someObject) // returns undefined when intermediate properties are not defined: Object.resolve('properties.that.do.not.exist', {hello:'world'}) 

Limites:

  • Impossible d'utiliser des crochets ( [] ) pour les indices de tableau (bien que la spécification des indices de tableau entre les périodes fonctionne bien comme indiqué ci-dessus)
  • Les propriétés avec des périodes dans leur nom ne peuvent pas être consultées: {'my.favorite.number':42}

Ceci est maintenant pris en charge par lodash en utilisant _.get(obj, property) . Voir https://lodash.com/docs#get

Exemple des documents:

 var object = { 'a': [{ 'b': { 'c': 3 } }] }; _.get(object, 'a[0].b.c'); // → 3 _.get(object, ['a', '0', 'b', 'c']); // → 3 _.get(object, 'abc', 'default'); // → 'default' 

Vous devriez analyser la chaîne vous-même:

 function getProperty(obj, prop) { var parts = prop.split('.'); if (Array.isArray(parts)) { var last = parts.pop(), l = parts.length, i = 1, current = parts[0]; while((obj = obj[current]) && i < l) { current = parts[i]; i++; } if(obj) { return obj[last]; } } else { throw 'parts is not valid array'; } } 

Cela exige que vous définissiez également les index de tableaux avec une notation par points:

 var part3name1 = "part3.0.name"; 

Cela rend l'analyse plus facile.

DEMO

Fonctionne également pour des tableaux / tableaux à l'intérieur de l'objet. Défensif contre les valeurs non valides.

 /** * Retrieve nested item from object/array * @param {Object|Array} obj * @param {String} path dot separated * @param {*} def default value ( if result undefined ) * @returns {*} */ function path(obj, path, def){ var i, len; for(i = 0,path = path.split('.'), len = path.length; i < len; i++){ if(!obj || typeof obj !== 'object') return def; obj = obj[path[i]]; } if(obj === undefined) return def; return obj; } ////////////////////////// // TEST // ////////////////////////// var arr = [true, {'sp ace': true}, true] var obj = { 'sp ace': true, arr: arr, nested: {'dotted.str.ing': true}, arr3: arr } shouldThrow(`path(obj, "arr.0")`); shouldBeDefined(`path(obj, "arr[0]")`); shouldBeEqualToNumber(`path(obj, "arr.length")`, 3); shouldBeTrue(`path(obj, "sp ace")`); shouldBeEqualToString(`path(obj, "none.existed.prop", "fallback")`, "fallback"); shouldBeTrue(`path(obj, "nested['dotted.str.ing'])`); 
 <script src="https://cdn.rawgit.com/coderek/e7b30bac7634a50ad8fd/raw/174b6634c8f57aa8aac0716c5b7b2a7098e03584/js-test.js"></script> 

En utilisant eval:

 var part1name = eval("someObject.part1.name"); 

Envelopper pour renvoyer indéfini en cas d'erreur

 function path(obj, path) { try { return eval("obj." + path); } catch(e) { return undefined; } } 

http://jsfiddle.net/shanimal/b3xTw/

Utilisez le bon sens et la prudence lorsque vous brandissez la puissance d'eval. C'est un peu comme un sabre léger, si vous l'activez, il y a 90% de chances que vous sépariez un membre. Ce n'est pas pour tout le monde.

Ici, j'offre plus de façons, ce qui semble plus rapide à bien des égards:

Option 1: chaîne divisée sur. Ou [ou] ou 'ou ", inversez-le, sautez des objets vides.

 function getValue(path, origin) { if (origin === void 0 || origin === null) origin = self ? self : this; if (typeof path !== 'string') path = '' + path; var parts = path.split(/\[|\]|\.|'|"/g).reverse(), name; // (why reverse? because it's usually faster to pop off the end of an array) while (parts.length) { name=parts.pop(); if (name) origin=origin[name]; } return origin; } 

Option 2 (le plus rapide de tous, à l'exception de eval ): balayage de caractères à faible niveau (sans regex / split / etc, juste une analyse de caractères rapide). Remarque: celui-ci ne prend pas en charge les citations pour les index.

 function getValue(path, origin) { if (origin === void 0 || origin === null) origin = self ? self : this; if (typeof path !== 'string') path = '' + path; var c = '', pc, i = 0, n = path.length, name = ''; if (n) while (i<=n) ((c = path[i++]) == '.' || c == '[' || c == ']' || c == void 0) ? (name?(origin = origin[name], name = ''):(pc=='.'||pc=='['||pc==']'&&c==']'?i=n+2:void 0),pc=c) : name += c; if (i==n+2) throw "Invalid path: "+path; return origin; } // (around 1,000,000+/- ops/sec) 

Option 3: ( nouvelle : option 2 développée pour soutenir les guillemets – un peu plus lente, mais toujours rapide)

 function getValue(path, origin) { if (origin === void 0 || origin === null) origin = self ? self : this; if (typeof path !== 'string') path = '' + path; var c, pc, i = 0, n = path.length, name = '', q; while (i<=n) ((c = path[i++]) == '.' || c == '[' || c == ']' || c == "'" || c == '"' || c == void 0) ? (c==q&&path[i]==']'?q='':q?name+=c:name?(origin?origin=origin[name]:i=n+2,name='') : (pc=='['&&(c=='"'||c=="'")?q=c:pc=='.'||pc=='['||pc==']'&&c==']'||pc=='"'||pc=="'"?i=n+2:void 0), pc=c) : name += c; if (i==n+2 || name) throw "Invalid path: "+path; return origin; } 

JSPerf: http://jsperf.com/ways-to-dereference-a-delimited-property-string/3

"Eval (…)" est encore roi cependant (performance sage qui est). Si vous avez des chemins de propriété directement sous votre contrôle, il ne devrait pas y avoir de problème lors de l'utilisation de 'eval' (surtout si la vitesse est souhaitée). Si vous tirez sur les chemins de propriété "sur le fil" ( sur la ligne !? Lol: P), alors oui, utilisez autre chose pour être en sécurité. Seul un idiot dirait de ne jamais utiliser "eval", car il y a de bonnes raisons de l'utiliser. De plus, "Il est utilisé dans l'analyseur JSON de Doug Crockford ". Si l'entrée est sécurisée, alors vous n'avez aucun problème. Utilisez le bon outil pour le bon travail, c'est tout.

Vous pouvez obtenir la valeur d'un membre de l'objet en profondeur avec une notation de points sans aucune bibliothèque de JavaScript externe avec la simple astuce suivante:

 new Function('_', 'return _.' + path)(obj); 

Dans votre cas pour obtenir la valeur de part1.name de someObject suffit de faire:

 new Function('_', 'return _.part1.name')(someObject); 

Voici une démonstration de violon simple: https://jsfiddle.net/harishanchu/oq5esowf/

ES6 : Une seule ligne à Vanila JS (elle retourne nulle si elle ne trouve pas au lieu de donner une erreur):

 'path.string'.split('.').reduce((p,c)=>p&&p[c]||null, MyOBJ) 

Ou exemple:

 'abc'.split('.').reduce((p,c)=>p&&p[c]||null, {a:{b:{c:1}}}) 

Pour une fonction prête à l'emploi qui reconnaît également le nombre faux, 0 et négatif et accepte les valeurs par défaut comme paramètre:

 const resolvePath = (object, path, defaultValue) => path .split('.') .reduce((o, p) => o ? o[p] : defaultValue, object) 

Exemple à utiliser:

 resolvePath(window,'document.body') => <body> resolvePath(window,'document.body.xyz') => undefined resolvePath(window,'document.body.xyz', null) => null resolvePath(window,'document.body.xyz', 1) => 1 

Bonus :

Pour définir un chemin (Demandé par @ rob-gordon), vous pouvez utiliser:

 const setPath = (object, path, value) => path .split('.') .reduce((o,p) => o[p] = path.split('.').pop() === p ? value : o[p] || {}, object) 

Exemple:

 let myVar = {} setPath(myVar, 'abc', 42) => 42 console.log(myVar) => {a: {b: {c: 42}}} 

Je pense que vous demandez ceci:

 var part1name = someObject.part1.name; var part2quantity = someObject.part2.qty; var part3name1 = someObject.part3[0].name; 

Vous pourriez demander ceci:

 var part1name = someObject["part1"]["name"]; var part2quantity = someObject["part2"]["qty"]; var part3name1 = someObject["part3"][0]["name"]; 

Les deux fonctionneront


Ou peut-être que vous demandez cela

 var partName = "part1"; var nameStr = "name"; var part1name = someObject[partName][nameStr]; 

Enfin, vous pourriez demander cela

 var partName = "part1.name"; var partBits = partName.split("."); var part1name = someObject[partBits[0]][partBits[1]]; 

L'approche de Speigg est très propre et propre, bien que j'ai trouvé cette réponse tout en recherchant la solution d'accès aux propriétés de l'objectif AngularJS $ par chemin de chaîne et avec une petite modification, elle effectue le travail:

 $scope.resolve = function( path, obj ) { return path.split('.').reduce( function( prev, curr ) { return prev[curr]; }, obj || this ); } 

Il suffit de placer cette fonction dans votre contrôleur racine et de l'utiliser n'importe quel enfant comme ceci:

 $scope.resolve( 'path.to.any.object.in.scope') 

Voici les tests de performance pour tous les 4, avec @TheZver et @Shanimal étant les gagnants:

http://jsfiddle.net/Jw8XB/3/

 Part 1 60 Part 3A Object.byString: 2.536ms Part 1 60 Part 3A getProperty: 0.274ms Part 1 60 undefined eval: 0.657ms Part 1 60 Part 3A path: 0.256ms 

Je n'ai pas encore trouvé de paquet pour effectuer toutes les opérations avec un chemin de chaîne, alors j'ai fini par écrire mon propre petit paquet rapide qui supporte insert (), get () (avec retour par défaut), set () et supprimer ( ) Opérations.

Vous pouvez utiliser la notation de points, les parenthèses, les indices numériques, les propriétés de numéro de chaîne et les touches avec des caractères non-mots. Utilisation simple ci-dessous:

 > var jsocrud = require('jsocrud'); ... // Get (Read) --- > var obj = { > foo: [ > { > 'key w/ non-word chars': 'bar' > } > ] > }; undefined > jsocrud.get(obj, '.foo[0]["key w/ non-word chars"]'); 'bar' 

https://www.npmjs.com/package/jsocrud

https://github.com/vertical-knowledge/jsocrud

Il existe maintenant un module npm pour ce faire: https://github.com/erictrinh/safe-access

Exemple d'utilisation:

 var access = require('safe-access'); access(very, 'nested.property.and.array[0]'); 

C'est une doublure avec une allumette.

 const deep = { l1: { l2: { l3: "Hello" } } }; const prop = "l1.l2.l3"; const val = _.reduce(prop.split('.'), function(result, value) { return result ? result[value] : undefined; }, deep); // val === "Hello" 

Plunkr

Si vous devez accéder à différentes clés imbriquées sans le savoir au moment du codage (il sera trivial de les adresser), vous pouvez utiliser l'accessoire de notation de tableau:

 var part1name = someObject['part1']['name']; var part2quantity = someObject['part2']['qty']; var part3name1 = someObject['part3'][0]['name']; 

Ils sont équivalents à l'accesseur de notation par points et peuvent varier en cas d'exécution, par exemple:

 var part = 'part1'; var property = 'name'; var part1name = someObject[part][property]; 

est équivalent à

 var part1name = someObject['part1']['name']; 

ou

 var part1name = someObject.part1.name; 

J'espère que cette adresse est votre question …

MODIFIER

Je n'utiliserai pas une chaîne pour conserver une sorte de requête xpath pour accéder à une valeur d'objet. Comme vous devez appeler une fonction pour analyser la requête et récupérer la valeur, je suivrais un autre chemin (pas:

 var part1name = function(){ return this.part1.name; } var part2quantity = function() { return this['part2']['qty']; } var part3name1 = function() { return this.part3[0]['name'];} // usage: part1name.apply(someObject); 

Ou, si vous êtes mal à l'aise avec la méthode d' application

 var part1name = function(obj){ return obj.part1.name; } var part2quantity = function(obj) { return obj['part2']['qty']; } var part3name1 = function(obj) { return obj.part3[0]['name'];} // usage: part1name(someObject); 

Les fonctions sont plus courtes, plus claires, l'interprète les vérifie pour les erreurs de syntaxe, etc.

En passant, je pense qu'une simple tâche faite au bon moment suffira …

Il a simplement eu la même question récemment et utilisé avec succès https://npmjs.org/package/tea-properties qui set également les objets / arrachages imbriqués:

obtenir:

 var o = { prop: { arr: [ {foo: 'bar'} ] } }; var properties = require('tea-properties'); var value = properties.get(o, 'prop.arr[0].foo'); assert(value, 'bar'); // true 

ensemble:

 var o = {}; var properties = require('tea-properties'); properties.set(o, 'prop.arr[0].foo', 'bar'); assert(o.prop.arr[0].foo, 'bar'); // true 

Qu'en est-il de cette solution:

 setJsonValue: function (json, field, val) { if (field !== undefined){ try { eval("json." + field + " = val"); } catch(e){ ; } } } 

Et celui-ci, pour obtenir:

 getJsonValue: function (json, field){ var value = undefined; if (field !== undefined) { try { eval("value = json." + field); } catch(e){ ; } } return value; }; 

Probablement certains les considéreront dangereux, mais ils doivent être beaucoup plus rapides, puisqu'ils analysent la chaîne.

Le soulignement a une fonction disponible appelée getNested(obj, chain, def, opts) vue ici …

https://github.com/dsc/underscore.nested/blob/master/underscore.nested.js#L182

Avec coffeescript, vous pouvez utiliser le ? Symbole pour vérifier si l'objet existe, et si oui, suivez la chaîne comme ceci:

 part1name = someObject.part1?.name; part2quantity = someObject.part2?.qty; part3name1 = part3?[0]?.name; 

Si une propriété sur la chaîne n'existe pas, la valeur sera indéfinie 🙂

Les solutions ici sont juste pour accéder aux clés profondément imbriquées. J'avais besoin d'un pour accéder, ajouter, modifier et supprimer les clés. C'est ce que j'ai proposé:

 var deepAccessObject = function(object, path_to_key, type_of_function, value){ switch(type_of_function){ //Add key/modify key case 0: if(path_to_key.length === 1){ if(value) object[path_to_key[0]] = value; return object[path_to_key[0]]; }else{ if(object[path_to_key[0]]) return deepAccessObject(object[path_to_key[0]], path_to_key.slice(1), type_of_function, value); else object[path_to_key[0]] = {}; } break; //delete key case 1: if(path_to_key.length === 1){ delete object[path_to_key[0]]; return true; }else{ if(object[path_to_key[0]]) return deepAccessObject(object[path_to_key[0]], path_to_key.slice(1), type_of_function, value); else return false; } break; default: console.log("Wrong type of function"); } }; 
  • path_to_key : chemin dans un tableau. Vous pouvez le remplacer par votre string_path.split(".") .
  • type_of_function : 0 pour accéder (ne passe aucune valeur à la value ), 0 pour ajouter et modifier. 1 pour supprimer.
 /** * Access a deep value inside a object * Works by passing a path like "foo.bar", also works with nested arrays like "foo[0][1].baz" * @author Victor B. https://gist.github.com/victornpb/4c7882c1b9d36292308e * Unit tests: http://jsfiddle.net/Victornpb/0u1qygrh/ */ function getDeepVal(obj, path) { if (typeof obj === "undefined" || obj === null) return; path = path.split(/[\.\[\]\"\']{1,2}/); for (var i = 0, l = path.length; i < l; i++) { if (path[i] === "") continue; obj = obj[path[i]]; if (typeof obj === "undefined" || obj === null) return; } return obj; } 

Marche avec

 getDeepVal(obj,'foo.bar') getDeepVal(obj,'foo.1.bar') getDeepVal(obj,'foo[0].baz') getDeepVal(obj,'foo[1][2]') getDeepVal(obj,"foo['bar'].baz") getDeepVal(obj,"foo['bar']['baz']") getDeepVal(obj,"foo.bar.0.baz[1]['2']['w'].aaa[\"f\"].bb") 

Fonction simple, permettant une chaîne ou un chemin de tableau.

 function get(obj, path) { if(typeof path === 'string') path = path.split('.'); if(path.length === 0) return obj; return get(obj[path[0]], path.slice(1)); } const obj = {a: {b: {c: 'foo'}}}; console.log(get(obj, 'abc')); //foo 

OU

 console.log(get(obj, ['a', 'b', 'c'])); //foo 

Élaborer la réponse d'Alnitak:

 if(!Object.prototype.byString){ //NEW byString which can update values Object.prototype.byString = function(s, v, o) { var _o = o || this; s = s.replace(/\[(\w+)\]/g, '.$1'); // CONVERT INDEXES TO PROPERTIES s = s.replace(/^\./, ''); // STRIP A LEADING DOT var a = s.split('.'); //ARRAY OF STRINGS SPLIT BY '.' for (var i = 0; i < a.length; ++i) {//LOOP OVER ARRAY OF STRINGS var k = a[i]; if (k in _o) {//LOOP THROUGH OBJECT KEYS if(_o.hasOwnProperty(k)){//USE ONLY KEYS WE CREATED if(v !== undefined){//IF WE HAVE A NEW VALUE PARAM if(i === a.length -1){//IF IT'S THE LAST IN THE ARRAY _o[k] = v; } } _o = _o[k];//NO NEW VALUE SO JUST RETURN THE CURRENT VALUE } } else { return; } } return _o; }; 

}

Cela vous permet de définir une valeur aussi bien!

J'ai créé un paquet npm et github avec cela aussi