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


214

Étant donné un objet JS

var obj = { a: { b: '1', c: '2' } }

et une chaîne

"a.b"

comment puis-je convertir la chaîne en notation par points afin que je puisse aller

var val = obj.a.b

Si la chaîne était juste 'a', je pourrais l'utiliser obj[a]. Mais c'est plus complexe. J'imagine qu'il existe une méthode simple mais elle s'échappe actuellement.


25
@Andrey evalest mauvais; ne l'utilisez pas
kevinji

5
FYI: Voici quelques tests de vitesse intéressants que je viens de faire: jsperf.com/dereference-object-property-path-from-string
James Wilkins

si la perf est une considération sérieuse et que vous réutilisez beaucoup les mêmes chemins (par exemple à l'intérieur d'une fonction de filtre de tableau), utilisez le constructeur Function comme décrit dans ma réponse ci-dessous. Lorsque le même chemin est utilisé des milliers de fois, la méthode Function peut être plus de 10 fois plus rapide que l'évaluation ou la division et la réduction du chemin à chaque déréférence.
Kevin Crumley


Réponses:


437

note récente: Bien que je sois flatté que cette réponse ait suscité de nombreux votes positifs, je suis également quelque peu horrifié. Si l'on a besoin de convertir des chaînes de notation à points comme "xabc" en références, cela pourrait (peut-être) être un signe qu'il se passe quelque chose de très mal (sauf si vous effectuez une désérialisation étrange).

C'est-à-dire que les novices qui trouvent leur chemin vers cette réponse doivent se poser la question "pourquoi est-ce que je fais ça?"

Il est bien sûr généralement bien de le faire si votre cas d'utilisation est petit et que vous ne rencontrerez pas de problèmes de performances, ET vous n'aurez pas besoin de vous appuyer sur votre abstraction pour la rendre plus compliquée plus tard. En fait, si cela réduit la complexité du code et simplifie les choses, vous devriez probablement aller de l'avant et faire ce que demande OP. Cependant, si ce n'est pas le cas, considérez si l'une de ces conditions s'applique:

cas 1 : comme méthode principale de travail avec vos données (par exemple comme forme par défaut de votre application pour passer des objets et les déréférencer). Comme demander "comment puis-je rechercher un nom de fonction ou de variable dans une chaîne".

  • Il s'agit d'une mauvaise pratique de programmation (métaprogrammation inutile en particulier, et en quelque sorte enfreint le style de codage sans effet secondaire de la fonction, et aura des résultats de performance). Les novices qui se trouvent dans ce cas, devraient plutôt envisager de travailler avec des représentations de tableaux, par exemple ['x', 'a', 'b', 'c'], ou même quelque chose de plus direct / simple / simple si possible: comme ne pas perdre suivre les références elles-mêmes en premier lieu (idéal si ce n'est que du côté client ou uniquement du côté serveur), etc. (Un identifiant unique préexistant serait inélégant à ajouter, mais pourrait être utilisé si la spécification requiert son existence indépendamment.)

cas 2 : Travailler avec des données sérialisées ou des données qui seront affichées pour l'utilisateur. Comme utiliser une date comme une chaîne "1999-12-30" plutôt qu'un objet Date (ce qui peut entraîner des bogues de fuseau horaire ou ajouter de la complexité de sérialisation si cela n'est pas prudent). Ou vous savez ce que vous faites.

  • C'est peut-être bien. Veillez à ce qu'il n'y ait pas de chaîne de points "." dans vos fragments d'entrée aseptisés.

Si vous vous retrouvez à utiliser cette réponse tout le temps et à effectuer des conversions entre chaîne et tableau, vous pouvez être dans le mauvais cas et devriez envisager une alternative.

Voici une élégante doublure 10 fois plus courte que les autres solutions:

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

[modifier] Ou dans ECMAScript 6:

'a.b.etc'.split('.').reduce((o,i)=>o[i], obj)

(Ce n'est pas que je pense qu'eval soit toujours mauvais comme d'autres le suggèrent (bien que ce soit généralement le cas), néanmoins ces personnes seront ravies que cette méthode n'utilise pas eval. Ce qui précède trouvera la obj.a.b.etcdonnée objet la chaîne "a.b.etc".)

En réponse à ceux qui ont encore peur d'utiliser reducemalgré le fait qu'il soit dans la norme ECMA-262 (5ème édition), voici une implémentation 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('a.b.etc')

En fonction des optimisations effectuées par le compilateur JS, vous pouvez vous assurer que les fonctions imbriquées ne sont pas redéfinies à chaque appel via les méthodes habituelles (en les plaçant dans une fermeture, un objet ou un espace de noms global).

modifier :

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

Comment transformeriez-vous cela en setter? Non seulement renvoyer les valeurs par chemin, mais aussi les définir si une nouvelle valeur est envoyée dans la fonction? - Swader 28 juin à 21:42

(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 de style setter générale avec des effets secondaires comme le index(obj,"a.b.etc", value)faire obj.a.b.etc = value.)

Le reducestyle n'est pas vraiment adapté à cela, mais on peut modifier l'implémentation 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);
}

Démo:

> obj = {a:{b:{etc:5}}}

> index(obj,'a.b.etc')
5
> index(obj,['a','b','etc'])   #works with both strings and lists
5

> index(obj,'a.b.etc', 123)    #setter-mode - third argument (possibly poor form)
123

> index(obj,'a.b.etc')
123

... mais personnellement, je recommanderais de créer une fonction distincte setIndex(...). Je voudrais terminer sur une note secondaire que le poseur original de la question pourrait (devrait?) Travailler avec des tableaux d'indices (dont ils peuvent obtenir .split), plutôt qu'avec des chaînes; bien qu'il n'y ait généralement rien de mal avec une fonction pratique.


Un intervenant a demandé:

qu'en est-il des tableaux? quelque chose comme "ab [4] .cd [1] [2] [3]"? –AlexS

Javascript est un langage très étrange; en général, les objets ne peuvent avoir que des chaînes comme clés de propriété, par exemple, si xc'était un objet générique comme x={}, alors x[1]deviendrait x["1"]... vous lisez bien ... ouais ...

Les tableaux Javascript (qui sont eux-mêmes des instances d'Object) encouragent spécifiquement les clés entières, même si vous pouvez faire quelque chose comme x=[]; x["puppy"]=5; .

Mais en général (et il y a des exceptions), x["somestring"]===x.somestring(quand c'est autorisé; vous ne pouvez pas le faire x.123).

(Gardez à l'esprit que le compilateur JS que vous utilisez peut choisir, peut-être, de les compiler en représentations plus saines s'il peut prouver qu'il ne violerait pas les spécifications.)

Ainsi, la réponse à votre question dépendra du fait que vous supposez que ces objets acceptent uniquement des entiers (en raison d'une restriction dans votre domaine problématique), ou non. Supposons que non. Alors une expression valide est une concaténation d'un identifiant de base plus quelques .identifiers plus quelques ["stringindex"]s

Ce serait alors équivalent à a["b"][4]["c"]["d"][1][2][3], même si nous devrions probablement également soutenir a.b["c\"validjsstringliteral"][3]. Vous devez vérifier la section de grammaire ecmascript sur les littéraux de chaîne pour voir comment analyser un littéral de chaîne valide. Techniquement, vous voudriez également vérifier (contrairement à ma première réponse) qui aest un identifiant javascript valide .

Cependant, une réponse simple à votre question, si vos chaînes ne contiennent pas de virgules ou de crochets , serait simplement de faire correspondre des séquences de caractères de longueur 1+ et non dans l'ensemble ,ou [ou ]:

> "abc[4].c.def[1][2][\"gh\"]".match(/[^\]\[.]+/g)
// ^^^ ^  ^ ^^^ ^  ^   ^^^^^
["abc", "4", "c", "def", "1", "2", ""gh""]

Si vos chaînes ne contiennent pas de caractères d'échappement ou de "caractères , et parce que IdentifierNames sont un sous-langage de StringLiterals (je pense ???), vous pouvez d'abord convertir vos points en []:

> var R=[], demoString="abc[4].c.def[1][2][\"gh\"]";
> for(var match,matcher=/^([^\.\[]+)|\.([^\.\[]+)|\["([^"]+)"\]|\[(\d+)\]/g; 
      match=matcher.exec(demoString); ) {
  R.push(Array.from(match).slice(1).filter(x=>x!==undefined)[0]);
  // extremely bad code because js regexes are weird, don't use this
}
> R

["abc", "4", "c", "def", "1", "2", "gh"]

Bien sûr, soyez toujours prudent et ne faites jamais confiance à vos données. Certaines mauvaises façons de le faire qui pourraient fonctionner pour certains cas d'utilisation incluent également:

// hackish/wrongish; preprocess your string into "a.b.4.c.d.1.2.3", e.g.: 
> yourstring.replace(/]/g,"").replace(/\[/g,".").split(".")
"a.b.4.c.d.1.2.3"  //use code from before

Édition spéciale 2018:

Faisons le tour complet et faisons la solution la plus inefficace et horriblement surprogrammée que nous pouvons trouver ... dans l'intérêt du hamfistery de pureté syntaxique . Avec les objets proxy ES6! ... Définissons également certaines propriétés qui (à mon humble avis sont belles et merveilleuses mais) peuvent casser des bibliothèques mal écrites. Vous devriez peut-être vous méfier de l'utiliser si vous vous souciez de la performance, de la raison (la vôtre ou celle des autres), de votre travail, etc.

// [1,2,3][-1]==3 (or just use .slice(-1)[0])
if (![1][-1])
    Object.defineProperty(Array.prototype, -1, {get() {return this[this.length-1]}}); //credit to caub

// WARNING: THIS XTREME™ RADICAL METHOD IS VERY INEFFICIENT,
// ESPECIALLY IF INDEXING INTO MULTIPLE OBJECTS,
// because you are constantly creating wrapper objects on-the-fly and,
// even worse, going through Proxy i.e. runtime ~reflection, which prevents
// compiler optimization

// Proxy handler to override obj[*]/obj.* and obj[*]=...
var hyperIndexProxyHandler = {
    get: function(obj,key, proxy) {
        return key.split('.').reduce((o,i)=>o[i], obj);
    },
    set: function(obj,key,value, proxy) {
        var keys = key.split('.');
        var beforeLast = keys.slice(0,-1).reduce((o,i)=>o[i], obj);
        beforeLast[keys[-1]] = value;
    },
    has: function(obj,key) {
        //etc
    }
};
function hyperIndexOf(target) {
    return new Proxy(target, hyperIndexProxyHandler);
}

Démo:

var obj = {a:{b:{c:1, d:2}}};
console.log("obj is:", JSON.stringify(obj));

var objHyper = hyperIndexOf(obj);
console.log("(proxy override get) objHyper['a.b.c'] is:", objHyper['a.b.c']);
objHyper['a.b.c'] = 3;
console.log("(proxy override set) objHyper['a.b.c']=3, now obj is:", JSON.stringify(obj));

console.log("(behind the scenes) objHyper is:", objHyper);

if (!({}).H)
    Object.defineProperties(Object.prototype, {
        H: {
            get: function() {
                return hyperIndexOf(this); // TODO:cache as a non-enumerable property for efficiency?
            }
        }
    });

console.log("(shortcut) obj.H['a.b.c']=4");
obj.H['a.b.c'] = 4;
console.log("(shortcut) obj.H['a.b.c'] is obj['a']['b']['c'] is", obj.H['a.b.c']);

Production:

obj est: {"a": {"b": {"c": 1, "d": 2}}}

(proxy override get) objHyper ['abc'] est: 1

(proxy override set) objHyper ['abc'] = 3, maintenant obj est: {"a": {"b": {"c": 3, "d": 2}}}

(dans les coulisses) objHyper est: Proxy {a: {…}}

(raccourci) obj.H ['abc'] = 4

(raccourci) obj.H ['abc'] est obj ['a'] ['b'] ['c'] est: 4

idée inefficace: vous pouvez modifier ce qui précède pour répartir en fonction de l'argument d'entrée; soit utiliser la .match(/[^\]\[.]+/g)méthode pour prendre en charge obj['keys'].like[3]['this'], ou si instanceof Array, alors il suffit d'accepter un tableau comme entrée comme keys = ['a','b','c']; obj.H[keys].


Par suggestion que vous souhaitiez peut-être gérer les indices non définis d'une manière plus douce de style NaN (par exemple, index({a:{b:{c:...}}}, 'a.x.c')renvoyer unError non défini plutôt que non saisi) ...:

1) Cela a du sens du point de vue "nous devrions retourner indéfini plutôt que de jeter une erreur" dans la situation d'index à 1 dimension ({}) ['eg'] == indéfini, donc "nous devons retourner indéfini plutôt que de jeter un erreur "dans la situation à N dimensions.

2) Cela n'a pas de sens du point de vue que nous faisons x['a']['x']['c'], ce qui échouerait avec une TypeError dans l'exemple ci-dessus.

Cela dit, vous feriez cela en remplaçant votre fonction de réduction par:

(o,i)=>o===undefined?undefined:o[i], ou (o,i)=>(o||{})[i] .

(Vous pouvez rendre cela plus efficace en utilisant une boucle for et en cassant / retournant chaque fois que le sous-résultat dans lequel vous souhaitez indexer la prochaine fois n'est pas défini, ou en utilisant un try-catch si vous vous attendez à ce que de tels échecs soient suffisamment rares.)


4
reducen'est pas pris en charge dans tous les navigateurs actuellement utilisés.
Ricardo Tomasi

14
@Ricardo: Array.reducefait partie de la norme ECMA-262. Si vous souhaitez vraiment prendre en charge les navigateurs obsolètes, vous pouvez définir Array.prototype.reducel'exemple d'implémentation donné quelque part (par exemple developer.mozilla.org/en/JavaScript/Reference/Global_Objects/… ).
ninjagecko

2
Oui, mais c'est assez facile de mettre les deux lignes dans une fonction. var setget = function( obj, path ){ function index( robj,i ) {return robj[i]}; return path.split('.').reduce( index, obj ); }
neff

3
J'adore cet exemple élégant, merci ninjagecko. Je l'ai étendu pour gérer la notation de style tableau, ainsi que les chaînes vides - voir mon exemple ici: jsfiddle.net/sc0ttyd/q7zyd
Sc0ttyD

2
@ninjagecko comment transformeriez-vous cela en setter? Non seulement renvoyant les valeurs par chemin, mais également les définissant si une nouvelle valeur est envoyée dans la fonction?
Swader

64

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

_.get (objet, chemin, [valeur par défaut])

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

4
Remarque: _.get(object, path)ne se casse pas si aucun chemin n'a été trouvé. 'a.b.etc'.split('.').reduce((o,i)=>o[i], obj)Est-ce que. Pour mon cas spécifique - pas pour chaque cas - exactement ce dont j'avais besoin. Merci!
MB

1
@ Mr.B. la dernière version de Lodash a un troisième argument, facultatif, pour defaultValue. La _.get()méthode renvoie la valeur par défaut si _.get()elle est résolue en undefined, alors définissez-la sur ce que vous voulez et surveillez la valeur que vous définissez.
2017

7
Pour tous ceux qui se demandent, il prend également en charge _.set(object, path, value).
Jeffrey Roosendaal

19

Un exemple un peu plus impliqué avec la récursivité.

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,'a.d.a.b')); //blah

19

vous pouvez également utiliser lodash.get

Il vous suffit d'installer ce paquet (npm i --save lodash.get) et de l'utiliser comme ceci:

const get = require('lodash.get');

const myObj = { user: { firstName: 'Stacky', lastName: 'Overflowy' }, id: 123 };

console.log(get(myObj, 'user.firstName')); // prints Stacky
console.log(get(myObj, 'id')); //prints  123

//You can also update values
get(myObj, 'user').firstName = John;

10

Si vous prévoyez de déréférencer le même chemin plusieurs fois, la création d'une fonction pour chaque chemin de notation par points a de loin les meilleures performances (en développant les tests de performance liés à James Wilkins dans les commentaires ci-dessus).

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

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

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

var setter = new Function("obj", "newval", "obj." + path + " = newval;");
setter(obj, "some new val");

1
si vous avez besoin de déréférencer les mêmes chemins à une longue distance l'un de l'autre, le lien jsperf.com ci-dessus montre un exemple de comment enregistrer et rechercher la fonction plus tard. Le fait d'appeler le constructeur Function est assez lent, donc le code de haute performance devrait mémoriser les résultats pour éviter de le répéter si possible.
Kevin Crumley


7

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.

const getValue = (object, keys) => keys.split('.').reduce((o, k) => (o || {})[k], object);

console.log(getValue({ a: { b: '1', c: '2' } }, 'a.b'));
console.log(getValue({ a: { b: '1', c: '2' } }, 'foo.bar.baz'));


6

Notez que si vous utilisez déjà Lodash, vous pouvez utiliser les fonctions propertyou get:

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

Le soulignement a également une propertyfonction mais il ne prend pas en charge la notation par points.


1
Semble Underscore ne prend pas en charge la notation par points. Réponse mise à jour.
Tamlyn

5

D'autres propositions sont un peu énigmatiques, alors j'ai pensé 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] } }

5

Vous pouvez utiliser la bibliothèque disponible à npm, ce qui simplifie ce processus. https://www.npmjs.com/package/dot-object

 var dot = require('dot-object');

var obj = {
 some: {
   nested: {
     value: 'Hi there!'
   }
 }
};

var val = dot.pick('some.nested.value', obj);
console.log(val);

// Result: Hi there!

1
Merci d'avoir porté cela à notre attention. Semble très utile.
neff

Et c'est pourquoi chaque projet injecte 10000 dépendances :-).
Marvin

4
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

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

Aussi, JSFiddle .


Génial, cela fonctionne un régal et est plus court que CD Sanchez. Merci.
neff

valeur (a, 'bbbbb') ne renvoie pas indéfini
frumbert

4

J'ai étendu la réponse élégante par ninjagecko afin que la fonction gère à la fois les références de style pointé et / ou tableau, et qu'une chaîne vide provoque le 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 travail jsFiddle ici: http://jsfiddle.net/sc0ttyd/q7zyd/


Vraiment une bonne solution. Il n'y a qu'un seul problème, il suppose que la []notation est toujours pour les tableaux. il peut aussi y avoir des clés d'objet représentées de cette façon, par exemple obj['some-problem/name'].list[1]Pour résoudre ce problème, j'ai dû mettre à jour une arr_dereffonction comme celle-ci javascript function arr_deref(o, ref, i) { return !ref ? o : (o[(ref.slice(0, i ? -1 : ref.length)).replace(/^['"]|['"]$/g, '')]); }
Serkan Yersen

1
Bien que, de nos jours, je ne le ferais pas. J'utiliserais Lodash: lodash.com/docs#get
Sc0ttyD

2
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, "a.b"); // 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, "a.k", function () { console.log("hello world"); });

find(obj, "a.k")(); // hello world

Merci pour toutes les réponses rapides. Je n'aime pas les solutions eval (). Cela et les articles similaires sont les meilleurs. Mais j'ai toujours un problème. J'essaie de définir la valeur obj.ab = nouvelle valeur. Pour être précis, la valeur de b est une fonction, je dois donc utiliser obj.ab (new_value). La fonction est appelée mais la valeur n'est pas définie. Je pense que c'est un problème de portée mais je continue de creuser. Je me rends compte que cela sort du cadre de la question initiale. Mon code utilise Knockout.js et b est un ko.observable.
neff

@nevf: J'ai ajouté une deuxième fonction qui, je pense, fait ce que vous voulez. Vous pouvez le personnaliser à votre guise en fonction du comportement que vous souhaitez (par exemple, doit-il créer les objets s'ils n'existent pas?, Etc.).
Cristian Sanchez

@nevf Mais le mien le fait avec une seule fonction. ; D
McKayla

merci pour la mise à jour que j'ai pu utiliser. @tylermwashburn - et merci pour votre mise en œuvre plus courte qui fonctionne également comme un régal. Avoir un grand w / e tout.
neff

2

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 Function crée un nouvel objet Function. En JavaScript, chaque fonction est en fait un objet Function. La syntaxe pour créer une fonction explicitement avec le constructeur de fonction est:

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

arguments(arg1 to argN)doit être une chaîne qui correspond à un identifiant javaScript valide etfunctionBody 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 chaîne pour récupérer le membre de l'objet avec la notation par points.

J'espère que ça aide.


Pouvez-vous expliquer précisément ce que cela fait? Et comment passeriez-vous 'ab' etc. en tant que paramètre de fonction?
nevf

1
Je lui ai donné un +1 mais JSLint prévient que "le constructeur de fonction est une forme d'évaluation" .
gabe

2

Réponse GET / SET qui fonctionne également en React Native (vous ne pouvez pas l'attribuer Object.prototypeactuellement):

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

1

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

De plus, cela nous permettra de faire Object.prop(obj, 'a.b', false)là où nous ne pouvions pas auparavant. Malheureusement, cela ne nous laissera toujours pas attribuer undefined... Je ne sais pas encore comment procéder.

/**
 * 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]
 }

1

Si vous souhaitez convertir tout objet contenant des clés de notation par points en une version en tableau de ces clés, vous pouvez l'utiliser.


Cela convertira quelque chose comme

{
  name: 'Andy',
  brothers.0: 'Bob'
  brothers.1: 'Steve'
  brothers.2: 'Jack'
  sisters.0: 'Sally'
}

à

{
  name: 'Andy',
  brothers: ['Bob', 'Steve', 'Jack']
  sisters: ['Sally']
}

convertDotNotationToArray(objectWithDotNotation) {

    Object.entries(objectWithDotNotation).forEach(([key, val]) => {

      // Is the key of dot notation 
      if (key.includes('.')) {
        const [name, index] = key.split('.');

        // If you have not created an array version, create one 
        if (!objectWithDotNotation[name]) {
          objectWithDotNotation[name] = new Array();
        }

        // Save the value in the newly created array at the specific index 
        objectWithDotNotation[name][index] = val;
        // Delete the current dot notation key val
        delete objectWithDotNotation[key];
      }
    });

}

Il n'a pas traité les valeurs avec "brothers.0.one" comme JSON.
Karthick

Avez-vous des suggestions pour le traitement de JSON plus complexes avec la collecte de tableaux.?
Karthick

0

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, 'a.d.a.b')); //returns blah

0

Oui, il a été 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 au même endroit, elles pourraient être utiles.
Alors, 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 une propriété imbriquée partout sans importer de module avec fonction ou fonction copier / coller.

UPD.Exemple:

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

UPD 2. L'extension suivante casse la mangouste dans mon projet. J'ai également lu que cela pourrait casser 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;
};

0

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

Implémentation 2 (en utilisant tableau 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.a.b.access('c','d'); // returns: 'abcd'
myobj.access('a','b','c','e',0); // returns: 11

il peut également gérer des objets à l'intérieur des tableaux comme pour

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

0

Voici ma solution étendue proposée par: ninjagecko

Pour moi, une simple notation de chaîne n'était pas suffisante, donc la version ci-dessous prend en charge 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)
}

0

Au risque de battre un cheval mort ... Je trouve cela très utile pour traverser des objets imbriqués pour référencer où vous en êtes 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'objets imbriqués. Notez que j'ai utilisé un tableau pour contenir le chemin. Il serait trivial de le modifier pour utiliser soit un chemin de chaîne soit un tableau. Notez également que vous pouvez attribuer "undefined" à la valeur, contrairement à certaines 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);
}


0

J'ai utilisé ce code dans mon projet

const getValue = (obj, arrPath) => (
  arrPath.reduce((x, y) => {
    if (y in x) return x[y]
    return {}
  }, obj)
)

Usage:

const obj = { id: { user: { local: 104 } } }
const path = [ 'id', 'user', 'local' ]
getValue(obj, path) // return 104

0

Quelques années plus tard, j'ai trouvé cela qui gère la portée et le tableau. par exemplea['b']["c"].d.etc

function getScopedObj(scope, str) {
  let obj=scope, arr;

  try {
    arr = str.split(/[\[\]\.]/) // split by [,],.
      .filter(el => el)             // filter out empty one
      .map(el => el.replace(/^['"]+|['"]+$/g, '')); // remove string quotation
    arr.forEach(el => obj = obj[el])
  } catch(e) {
    obj = undefined;
  }

  return obj;
}

window.a = {b: {c: {d: {etc: 'success'}}}}

getScopedObj(window, `a.b.c.d.etc`)             // success
getScopedObj(window, `a['b']["c"].d.etc`)       // success
getScopedObj(window, `a['INVALID']["c"].d.etc`) // undefined

-1

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

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

1
Cela ne fonctionne pas a.b.cet n'accomplit pas vraiment ce qu'ils veulent. Ils veulent la valeur, pas un evalchemin.
McKayla

Je l'ai maintenant corrigé pour qu'il fonctionne avec a.b.c, mais vous avez raison, apparemment, il voulait obtenir / définir la valeur de la propriété à obj.a.b. La question m'a dérouté, car il a dit qu'il voulait "convertir la chaîne" ....
Mark Eirich

Bon travail. :) C'était un peu vague. Vous avez cependant fait un bon travail de conversion.
McKayla

-1

voici mes 10 cents à la casse, la fonction ci-dessous sera / définie en fonction du chemin fourni, .. bien sûr, vous pouvez l'améliorer, supprimer || et remplacez-le par Object.hasOwnPropertysi vous vous souciez des fausses valeurs par erreur,

je l'ai testé avec a.b.cet ab2.c {a:{b:[0,1,{c:7}]}}et ses travaux pour le réglage et l'obtention :).

cheerz

function helper(obj, path, setValue){
  const l = String(path).split('.');
  return l.reduce((o,i, idx)=>{
   if( l.length-idx===1)  { o[i] = setValue || o[i];return setValue ? obj : o[i];}
  o[i] = o[i] || {};
   return o[i];
  }, x)
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.