Convertir la chaîne JavaScript en notation par points en une référence d'objet

Étant donné un objet JS: var obj = { a: { b: '1', c: '2' } } et une chaîne ab Comment puis-je convertir la chaîne en notation par points, donc je peux aller: var val = obj.ab ;

Si la chaîne était juste «a», je peux utiliser obj[a] mais c'est plus complexe. J'imagine qu'il y a une méthode simple, mais elle échappe à l'heure actuelle.

Note récente: Bien que je sois flatté que cette réponse ait eu beaucoup de réponses positives, je suis également quelque peu horrifiée. Si l'on a besoin de convertir des cordes de notation de point comme "xabc" en références, il est probablement un signe qu'il y a quelque chose de très faux (à moins que vous ne soyez peut-être en train de faire une étrange désérialisation). Il s'agit d'une destruction excessive car il s'agit d'une métaprogrammation inutile et constitue également une violation du style de codage fonctionnel sans effet fonctionnel. Aussi, attendez-vous à des résultats de performance massive si vous faites cela plus que vous ne le souhaitez (par exemple, la forme par défaut de votre application de passer des objets et de les désélectionner). Si, pour une raison ou une autre, il s'agit de js du côté du serveur, la solution habituelle pour la désinfection des intrants. Les novices qui trouvent leur chemin à cette réponse devraient envisager de travailler avec des représentations de tableau à la place, par exemple ['x', 'a', 'b', 'c'], ou même quelque chose de plus direct / simple / direct si possible, comme ne pas perdre Suivi des références elles-mêmes, ou peut-être un identifiant unique préexistant, etc.

Voici un élégant one-liner qui est 10 fois plus court que les autres solutions:

 function index(obj,i) {return obj[i]} 'abetc'.split('.').reduce(index, obj) 

[Edit] Ou dans ECMAScript 6:

 'abetc'.split('.').reduce((o,i)=>o[i], obj) 

(Ce n'est pas que je pense que eval est toujours mauvais, comme d'autres l'ont suggéré (bien que ce soit habituellement), mais ces gens se feront ravi que cette méthode n'utilise pas eval. Ce qui précède trouvera obj.abetc donné obj et la chaîne "abetc" .)

En réponse à ceux qui ont encore peur d'utiliser reduce malgré le fait qu'il soit dans la norme ECMA-262 (5ème édition), voici une implantation récursive à deux lignes:

 function multiIndex(obj,is) { // obj,['1','2','3'] -> ((obj['1'])['2'])['3'] return is.length ? multiIndex(obj[is[0]],is.slice(1)) : obj } function pathIndex(obj,is) { // obj,'1.2.3' -> multiIndex(obj,['1','2','3']) return multiIndex(obj,is.split('.')) } pathIndex('abetc') 

Selon les optimisations effectuées par le compilateur JS, vous voudrez peut-être vous assurer que toutes les fonctions imbriquées ne sont pas définies de nouveau sur chaque appel par les méthodes habituelles (les placer dans une fermeture, un objet ou un espace de noms global).

Modifier :

Pour répondre à une question intéressante dans les commentaires:

Comment allez-vous transformer cela en setter aussi? Non seulement le retour des valeurs par chemin, mais aussi le réglage si une nouvelle valeur est envoyée dans la fonction? – Swader 28 juin à 21h42

(Sidenote: malheureusement, ne peut pas retourner un objet avec un Setter, car cela violerait la convention d'appel; le commentateur semble plutôt se référer à une fonction générale de setter-style avec des effets secondaires comme l' index(obj,"abetc", value) Faisant obj.abetc = value .)

Le style reduce n'est pas vraiment adapté à cela, mais nous pouvons modifier la mise en œuvre récursive:

 function index(obj,is, value) { if (typeof is == 'string') return index(obj,is.split('.'), value); else if (is.length==1 && value!==undefined) return obj[is[0]] = value; else if (is.length==0) return obj; else return index(obj[is[0]],is.slice(1), value); } 

Demo:

 > obj = {a:{b:{etc:5}}} > index(obj,'abetc') 5 > index(obj,['a','b','etc']) #works with both strings and lists 5 > index(obj,'abetc', 123) #setter-mode - third argument (possibly poor form) 123 > index(obj,'abetc') 123 

… bien que personnellement, je recommanderais de créer une fonction séparée setIndex(...) . Je voudrais terminer sur une note de côté que le postier original de la question pourrait (devrait?) .split avec des tableaux d'indices (qu'ils peuvent obtenir de .split ) plutôt que de chaînes; Bien qu'il n'y ait généralement rien de mal à une fonction de commodité.

Si vous pouvez utiliser lodash , il existe une fonction qui fait exactement cela:

_.get (objet, chemin, [defaultValue])

 var val = _.get(obj, "ab"); 

Un exemple un peu plus impliqué avec récursion.

 function recompose(obj,string){ var parts = string.split('.'); var newObj = obj[parts[0]]; if(parts[1]){ parts.splice(0,1); var newString = parts.join('.'); return recompose(newObj,newString); } return newObj; } var obj = { a: { b: '1', c: '2', d:{a:{b:'blah'}}}}; alert(recompose(obj,'adab')); //blah 

Si vous prévoyez de différencier le même chemin à plusieurs reprises, construire une fonction pour chaque trace de notation de points a en fait la meilleure performance de loin (en développant les tests de performance James Wilkins liés aux commentaires ci-dessus).

 var path = 'abx'; var getter = new Function("obj", "return obj." + path + ";"); getter(obj); 

L'utilisation du constructeur de fonctions présente les mêmes inconvénients que eval () en termes de sécurité et de performances les plus défavorables, mais IMO est un outil mal utilisé pour les cas où vous avez besoin d'une combinaison de dynamisme extrême et de performances élevées. J'utilise cette méthodologie pour créer des fonctions de filtrage de réseau et les appeler dans une boucle Digest AngularJS. Mes profils montrent systématiquement le fichier array.filter () prenant moins de 1 ms pour déréférencer et filtrer environ 2000 objets complexes, en utilisant des chemins dynamiquement définis 3-4 niveaux de profondeur.

Une méthodologie similaire pourrait être utilisée pour créer des fonctions de setter, bien sûr:

 var setter = new Function("obj", "newval", "obj." + path + " = newval;"); setter(obj, "some new val"); 
 var a = { b: { c: 9 } }; function value(layer, path, value) { var i = 0, path = path.split('.'); for (; i < path.length; i++) if (value != null && i + 1 === path.length) layer[path[i]] = value; layer = layer[path[i]]; return layer; }; value(a, 'b.c'); // 9 value(a, 'b.c', 4); value(a, 'b.c'); // 4 

Ceci est beaucoup de code par rapport à la façon la plus simple de le faire, mais comme Simon Willison dit, vous ne devriez jamais utiliser eval .

Aussi, JSFiddle .

D'autres propositions sont un peu cryptiques, alors j'ai pensé que je pourrais contribuer:

 Object.prop = function(obj, prop, val){ var props = prop.split('.') , final = props.pop(), p while(p = props.shift()){ if (typeof obj[p] === 'undefined') return undefined; obj = obj[p] } return val ? (obj[final] = val) : obj[final] } var obj = { a: { b: '1', c: '2' } } // get console.log(Object.prop(obj, 'a.c')) // -> 2 // set Object.prop(obj, 'a.c', function(){}) console.log(obj) // -> { a: { b: '1', c: [Function] } } 

Notez si vous utilisez déjà Lodash, vous pouvez utiliser la property ou get fonctions:

 var obj = { a: { b: '1', c: '2' } }; _.property('a.b')(obj); // => 1 _.get(obj, 'a.b'); // => 1 

Le soulignement a également une fonction de property , mais il ne supporte pas la notation par points.

Beaucoup d'années depuis la publication originale. Maintenant, il existe une grande bibliothèque appelée «chemin d'objet». https://github.com/mariocasciaro/object-path

Disponible sur NPM et BOWER https://www.npmjs.com/package/object-path

C'est aussi simple que:

 objectPath.get(obj, "ac1"); //returns "f" objectPath.set(obj, "aj0.f", "m"); 

Et fonctionne pour des propriétés profondément imbriquées et des tableaux.

J'ai étendu la réponse élégante par ninjagecko afin que la fonction gère à la fois les références de style en pointillé et / ou de tableau, et de sorte qu'une chaîne vide provoque l'retour de l'objet parent.

Voici:

 string_to_ref = function (object, reference) { function arr_deref(o, ref, i) { return !ref ? o : (o[ref.slice(0, i ? -1 : ref.length)]) } function dot_deref(o, ref) { return ref.split('[').reduce(arr_deref, o); } return !reference ? object : reference.split('.').reduce(dot_deref, object); }; 

Voir mon exemple de jsFiddle en cours ici: http://jsfiddle.net/sc0ttyd/q7zyd/

Vous pouvez obtenir la valeur d'un membre d'objet par notation par points avec une seule ligne de code:

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

Dans votre cas:

 var obj = { a: { b: '1', c: '2' } } var val = new Function('_', 'return _.a.b')(obj); 

Pour simplifier, vous pouvez écrire une fonction comme celle-ci:

 function objGet(obj, path){ return new Function('_', 'return _.' + path)(obj); } 

Explication:

Le constructeur de fonctions crée un nouvel objet Function. Dans JavaScript, chaque fonction est en fait un objet Function. La syntaxe pour créer une fonction explicitement avec le constructeur de fonctions est:

 new Function ([arg1[, arg2[, ...argN]],] functionBody) 

Où les arguments(arg1 to argN) doivent être une chaîne qui correspond à un identifiant et une functionBody javaScript valide. Body est une chaîne contenant les instructions javaScript comprenant la définition de la fonction.

Dans notre cas, nous profitons du corps de la fonction de chaîne pour récupérer le membre de l'objet avec une notation par points.

J'espère que cela aide.

 var find = function(root, path) { var segments = path.split('.'), cursor = root, target; for (var i = 0; i < segments.length; ++i) { target = cursor[segments[i]]; if (typeof target == "undefined") return void 0; cursor = target; } return cursor; }; var obj = { a: { b: '1', c: '2' } } find(obj, "ab"); // 1 var set = function (root, path, value) { var segments = path.split('.'), cursor = root, target; for (var i = 0; i < segments.length - 1; ++i) { cursor = cursor[segments[i]] || { }; } cursor[segments[segments.length - 1]] = value; }; set(obj, "ak", function () { console.log("hello world"); }); find(obj, "ak")(); // hello world 

Je suggère de diviser le chemin et de l'itérer et de réduire l'objet que vous avez. Cette proposition fonctionne avec une valeur par défaut pour les propriétés manquantes.

 function getValue(object, keys) { return keys.split('.').reduce(function (o, k) { return (o || {})[k]; }, object); } console.log(getValue({ a: { b: '1', c: '2' } }, 'a.b')); console.log(getValue({ a: { b: '1', c: '2' } }, 'foo.bar.baz')); 

J'ai copié ce qui suit de la réponse de Ricardo Tomasi et j'ai modifié pour créer des sous-objets qui n'existent pas encore si nécessaire. C'est un peu moins efficace (plus if s et création d'objets vides), mais devrait être très bon.

De plus, cela nous permettra de faire Object.prop(obj, 'a.b', false) où nous ne pouvions pas auparavant. Malheureusement, il ne nous permettra pas d'assigner undefined … Pas sûr de comment aborder celui-là encore.

 /** * Object.prop() * * Allows dot-notation access to object properties for both getting and setting. * * @param {Object} obj The object we're getting from or setting * @param {string} prop The dot-notated string defining the property location * @param {mixed} val For setting only; the value to set */ Object.prop = function(obj, prop, val){ var props = prop.split('.'), final = props.pop(), p; for (var i = 0; i < props.length; i++) { p = props[i]; if (typeof obj[p] === 'undefined') { // If we're setting if (typeof val !== 'undefined') { // If we're not at the end of the props, keep adding new empty objects if (i != props.length) obj[p] = {}; } else return undefined; } obj = obj[p] } return typeof val !== "undefined" ? (obj[final] = val) : obj[final] } 

La réponse GET / SET qui fonctionne également en mode réactivé (vous ne pouvez pas attribuer à Object.prototype actuellement):

 Object.defineProperty(Object.prototype, 'getNestedProp', { value: function(desc) { var obj = this; var arr = desc.split("."); while(arr.length && (obj = obj[arr.shift()])); return obj; }, enumerable: false }); Object.defineProperty(Object.prototype, 'setNestedProp', { value: function(desc, value) { var obj = this; var arr = desc.split("."); var last = arr.pop(); while(arr.length && (obj = obj[arr.shift()])); obj[last] = value; }, enumerable: false }); 

Usage:

 var a = { values: [{ value: null }] }; var b = { one: { two: 'foo' } }; a.setNestedProp('values.0.value', b.getNestedProp('one.two')); console.log(a.values[0].value); // foo 

Il n'est pas clair quelle est votre question. Compte tenu de votre objet, obj.ab vous donnerait "2" tel quel. Si vous vouliez manipuler la chaîne pour utiliser des brackets, vous pouvez le faire:

 var s = 'a.b'; s = 'obj["' + s.replace(/\./g, '"]["') + '"]'; alert(s); // displays obj["a"]["b"] 

Voici mon code sans utiliser eval . C'est aussi facile à comprendre.

 function value(obj, props) { if (!props) return obj; var propsArr = props.split('.'); var prop = propsArr.splice(0, 1); return value(obj[prop], propsArr.join('.')); } var obj = { a: { b: '1', c: '2', d:{a:{b:'blah'}}}}; console.log(value(obj, 'adab')); //returns blah 

Oui, on l'a demandé il y a 4 ans et oui, l'extension des prototypes de base n'est généralement pas une bonne idée, mais si vous conservez toutes les extensions dans un endroit, elles pourraient être utiles.
Donc, voici ma façon de le faire.

  Object.defineProperty(Object.prototype, "getNestedProperty", { value : function (propertyName) { var result = this; var arr = propertyName.split("."); while (arr.length && result) { result = result[arr.shift()]; } return result; }, enumerable: false }); 

Maintenant, vous pourrez obtenir des propriétés imbriquées partout sans importer de module avec fonction ou copie / collage.

UPD.Exemple:

 {a:{b:11}}.getNestedProperty('a.b'); //returns 11 

UPD 2. L'extension suivante mongue dans mon projet. J'ai également lu que cela pourrait rompre jquery. Donc, ne le faites jamais de la manière suivante

  Object.prototype.getNestedProperty = function (propertyName) { var result = this; var arr = propertyName.split("."); while (arr.length && result) { result = result[arr.shift()]; } return result; }; 

Voici ma mise en œuvre

Mise en œuvre 1

 Object.prototype.access = function() { var ele = this[arguments[0]]; if(arguments.length === 1) return ele; return ele.access.apply(ele, [].slice.call(arguments, 1)); } 

Mise en œuvre 2 (en utilisant le tableau de réduire au lieu de tranche)

 Object.prototype.access = function() { var self = this; return [].reduce.call(arguments,function(prev,cur) { return prev[cur]; }, self); } 

Exemples:

 var myobj = {'a':{'b':{'c':{'d':'abcd','e':[11,22,33]}}}}; myobj.access('a','b','c'); // returns: {'d':'abcd', e:[0,1,2,3]} myobj.abaccess('c','d'); // returns: 'abcd' myobj.access('a','b','c','e',0); // returns: 11 

Il peut également gérer des objets dans des tableaux comme pour

 var myobj2 = {'a': {'b':[{'c':'ab0c'},{'d':'ab1d'}]}} myobj2.access('a','b','1','d'); // returns: 'ab1d' 

C'est ma solution étendue proposée par: ninjagecko

Pour moi, la notation de chaîne simple n'était pas suffisante, alors la version ci-dessous supporte des choses comme:

Index (obj, 'data.accounts [0] .address [0] .postcode');

 /** * Get object by index * @supported * - arrays supported * - array indexes supported * @not-supported * - multiple arrays * @issues: * index(myAccount, 'accounts[0].address[0].id') - works fine * index(myAccount, 'accounts[].address[0].id') - doesnt work * @Example: * index(obj, 'data.accounts[].id') => returns array of id's * index(obj, 'data.accounts[0].id') => returns id of 0 element from array * index(obj, 'data.accounts[0].addresses.list[0].id') => error * @param obj * @param path * @returns {any} */ var index = function(obj, path, isArray?, arrIndex?){ // is an array if(typeof isArray === 'undefined') isArray = false; // array index, // if null, will take all indexes if(typeof arrIndex === 'undefined') arrIndex = null; var _arrIndex = null; var reduceArrayTag = function(i, subArrIndex){ return i.replace(/(\[)([\d]{0,})(\])/, (i) => { var tmp = i.match(/(\[)([\d]{0,})(\])/); isArray = true; if(subArrIndex){ _arrIndex = (tmp[2] !== '') ? tmp[2] : null; }else{ arrIndex = (tmp[2] !== '') ? tmp[2] : null; } return ''; }); } function byIndex(obj, i) { // if is an array if(isArray){ isArray = false; i = reduceArrayTag(i, true); // if array index is null, // return an array of with values from every index if(!arrIndex){ var arrValues = []; _.forEach(obj, (el) => { arrValues.push(index(el, i, isArray, arrIndex)); }) return arrValues; } // if array index is specified var value = obj[arrIndex][i]; if(isArray){ arrIndex = _arrIndex; }else{ arrIndex = null; } return value; }else{ // remove [] from notation, // if [] has been removed, check the index of array i = reduceArrayTag(i, false); return obj[i] } } // reduce with byIndex method return path.split('.').reduce(byIndex, obj) } 

Au risque de battre un cheval mort … Je trouve cela très utile pour traverser des objets imbriqués pour faire référence à l'endroit où vous vous trouvez par rapport à l'objet de base ou à un objet similaire avec la même structure. À cette fin, cela est utile avec une fonction de traversée d'objet imbriquée. Notez que j'ai utilisé un tableau pour contenir le chemin. Il serait banal de modifier ceci pour utiliser soit un chemin de chaîne, soit un tableau. Notez également que vous pouvez assigner "indéfini" à la valeur, contrairement à certaines des autres implémentations.

 /* * Traverse each key in a nested object and call fn(curObject, key, value, baseObject, path) * on each. The path is an array of the keys required to get to curObject from * baseObject using objectPath(). If the call to fn() returns falsey, objects below * curObject are not traversed. Should be called as objectTaverse(baseObject, fn). * The third and fourth arguments are only used by recursion. */ function objectTraverse (o, fn, base, path) { path = path || []; base = base || o; Object.keys(o).forEach(function (key) { if (fn(o, key, o[key], base, path) && jQuery.isPlainObject(o[key])) { path.push(key); objectTraverse(o[key], fn, base, path); path.pop(); } }); } /* * Get/set a nested key in an object. Path is an array of the keys to reference each level * of nesting. If value is provided, the nested key is set. * The value of the nested key is returned. */ function objectPath (o, path, value) { var last = path.pop(); while (path.length && o) { o = o[path.shift()]; } if (arguments.length < 3) { return (o? o[last] : o); } return (o[last] = value); }