web-dev-qa-db-fra.com

Définir dynamiquement la propriété de l'objet imbriqué

J'ai un objet qui pourrait avoir un nombre quelconque de niveaux et qui pourrait avoir n'importe quelle propriété existante .Par exemple:

var obj = {
    db: {
        mongodb: {
            Host: 'localhost'
        }
    }
};

Sur ce je voudrais définir (ou écraser) des propriétés comme ceci:

set('db.mongodb.user', 'root');
// or:
set('foo.bar', 'baz');

Où la chaîne de propriété peut avoir n'importe quelle profondeur et la valeur peut être n'importe quel type/chose.
Les objets et les tableaux en tant que valeurs n'ont pas besoin d'être fusionnés, si la clé de propriété existe déjà.

L'exemple précédent produirait l'objet suivant:

var obj = {
    db: {
        mongodb: {
            Host: 'localhost',
            user: 'root'
        }
    },
    foo: {
        bar: baz
    }
};

Comment puis-je réaliser une telle fonction?

42
John B.

Cette fonction, en utilisant les arguments que vous avez spécifiés, devrait ajouter/mettre à jour les données dans le conteneur obj. Notez que vous devez savoir quels éléments du schéma obj sont des conteneurs et lesquels sont des valeurs (chaînes, entiers, etc.), sinon vous commencerez à générer des exceptions.

obj = {};  // global object

function set(path, value) {
    var schema = obj;  // a moving reference to internal objects within obj
    var pList = path.split('.');
    var len = pList.length;
    for(var i = 0; i < len-1; i++) {
        var elem = pList[i];
        if( !schema[elem] ) schema[elem] = {}
        schema = schema[elem];
    }

    schema[pList[len-1]] = value;
}

set('mongo.db.user', 'root');
62
bpmason1

Lodash a une méthode _.set () .

_.set(obj, 'db.mongodb.user', 'root');
_.set(obj, 'foo.bar', 'baz');
36
aheuermann

Un peu tard, mais voici une réponse plus simple:

/**
 * Dynamically sets a deeply nested value in an object.
 * Optionally "bores" a path to it if its undefined.
 * @function
 * @param {!object} obj  - The object which contains the value you want to change/set.
 * @param {!array} path  - The array representation of path to the value you want to change/set.
 * @param {!mixed} value - The value you want to set it to.
 * @param {boolean} setrecursively - If true, will set value of non-existing path as well.
 */
function setDeep(obj, path, value, setrecursively = false) {

    let level = 0;

    path.reduce((a, b)=>{
        level++;

        if (setrecursively && typeof a[b] === "undefined" && level !== path.length){
            a[b] = {};
            return a[b];
        }

        if (level === path.length){
            a[b] = value;
            return value;
        } else {
            return a[b];
        }
    }, obj);
}

Cette fonction que j'ai créée peut faire exactement ce dont vous avez besoin et un peu plus.

disons que nous voulons changer la valeur cible qui est profondément imbriquée dans cet objet:

let myObj = {
    level1: {
        level2: {
           target: 1
       }
    }
}

Nous appelons donc notre fonction comme suit:

setDeep(myObj, ["level1", "level2", "target1"], 3);

aura pour résultat:

myObj = { niveau 1: { niveau 2: { cible: 3 } } }

Définir l'indicateur set récursivement sur true définira les objets s'ils n'existent pas.

setDeep(myObj, ["new", "path", "target"], 3);

entraînera ceci:

obj = myObj = {
    new: {
         path: {
             target: 3
         }
    },
    level1: {
        level2: {
           target: 3
       }
    }
}
6
Philll_t

Inspiré par la réponse de @ bpmason1:

function leaf(obj, path, value) {
  const pList = path.split('.');
  const key = pList.pop();
  const pointer = pList.reduce((accumulator, currentValue) => {
    if (accumulator[currentValue] === undefined) accumulator[currentValue] = {};
    return accumulator[currentValue];
  }, obj);
  pointer[key] = value;
  return obj;
}

Exemple:

const obj = {
  boats: {
    m1: 'lady blue'
  }
};
leaf(obj, 'boats.m1', 'lady blue II');
leaf(obj, 'boats.m2', 'lady bird');
console.log(obj); // { boats: { m1: 'lady blue II', m2: 'lady bird' } }
6
webjay

Lodash a une méthode appelée update qui fait exactement ce dont vous avez besoin.

Cette méthode reçoit les paramètres suivants:

  1. L'objet à mettre à jour
  2. Le chemin de la propriété à mettre à jour (la propriété peut être profondément imbriquée)
  3. Une fonction qui renvoie la valeur à mettre à jour (en prenant la valeur d'origine en tant que paramètre)

Dans votre exemple, cela ressemblerait à ceci:

_.update(obj, 'db.mongodb.user', function(originalValue) {
  return 'root'
})
5
brafdlog

Nous pouvons utiliser une fonction de récursivité:

/**
 * Sets a value of nested key string descriptor inside a Object.
 * It changes the passed object.
 * Ex:
 *    let obj = {a: {b:{c:'initial'}}}
 *    setNestedKey(obj, ['a', 'b', 'c'], 'changed-value')
 *    assert(obj === {a: {b:{c:'changed-value'}}})
 *
 * @param {[Object]} obj   Object to set the nested key
 * @param {[Array]} path  An array to describe the path(Ex: ['a', 'b', 'c'])
 * @param {[Object]} value Any value
 */
export const setNestedKey = (obj, path, value) => {
  if (path.length === 1) {
    obj[path] = value
    return
  }
  return setNestedKey(obj[path[0]], path.slice(1), value)
}

C'est plus simple!

2
Hemã Vidal

J'ai créé Gist pour définir et obtenir des valeurs obj par chaîne en fonction de la réponse correcte. Vous pouvez le télécharger ou l’utiliser comme paquet npm/yarn.

// yarn add Gist:5ceba1081bbf0162b98860b34a511a92
// npm install Gist:5ceba1081bbf0162b98860b34a511a92
export const DeepObject = {
  set: setDeep,
  get: getDeep
};

// https://stackoverflow.com/a/6491621
function getDeep(obj: Object, path: string) {
  path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
  path = path.replace(/^\./, '');           // strip a leading dot
  const a = path.split('.');
  for (let i = 0, l = a.length; i < l; ++i) {
    const n = a[i];
    if (n in obj) {
      obj = obj[n];
    } else {
      return;
    }
  }

  return obj;
}

// https://stackoverflow.com/a/18937118
function setDeep(obj: Object, path: string, value: any) {
  let schema = obj;  // a moving reference to internal objects within obj
  const pList = path.split('.');
  const len = pList.length;
  for (let i = 0; i < len - 1; i++) {
    const elem = pList[i];
    if (!schema[elem]) {
      schema[elem] = {};
    }
    schema = schema[elem];
  }

  schema[pList[len - 1]] = value;
}

// Usage
// import {DeepObject} from 'somePath'
//
// const obj = {
//   a: 4,
//   b: {
//     c: {
//       d: 2
//     }
//   }
// };
//
// DeepObject.set(obj, 'b.c.d', 10); // sets obj.b.c.d to 10
// console.log(DeepObject.get(obj, 'b.c.d')); // returns 10
1
Chiffre

Je viens d'écrire une petite fonction en utilisant ES6 + récursivité pour atteindre l'objectif.

updateObjProp = (obj, value, propPath) => {
    const [head, ...rest] = propPath.split('.');

    !rest.length
        ? obj[head] = value
        : this.updateObjProp(obj[head], value, rest);
}

const user = {profile: {name: 'foo'}};
updateObjProp(user, 'fooChanged', 'profile.name');

Je l'ai beaucoup utilisé pour réagir à la mise à jour, cela a très bien fonctionné pour moi.

1
Bruno Joaquim

Si vous devez uniquement modifier des objets imbriqués plus profonds, une autre méthode pourrait consister à référencer l'objet. Les objets JS étant gérés par leurs références, vous pouvez créer une référence à un objet pour lequel vous disposez d'un accès clé-chaîne.

Exemple:

// The object we want to modify:
var obj = {
    db: {
        mongodb: {
            Host: 'localhost',
            user: 'root'
        }
    },
    foo: {
        bar: baz
    }
};

var key1 = 'mongodb';
var key2 = 'Host';

var myRef = obj.db[key1]; //this creates a reference to obj.db['mongodb']

myRef[key2] = 'my new string';

// The object now looks like:
var obj = {
    db: {
        mongodb: {
            Host: 'my new string',
            user: 'root'
        }
    },
    foo: {
        bar: baz
    }
};
1
aggregate1166877

ES6 a aussi un moyen très pratique de faire cela en utilisant Nom de la propriété calculée et Paramètre de repos .

const obj = {
  levelOne: {
    levelTwo: {
      levelThree: "Set this one!"
    }
  }
}

const updatedObj = {
  ...obj,
  levelOne: {
    ...obj.levelOne,
    levelTwo: {
      ...obj.levelOne.levelTwo,
      levelThree: "I am now updated!"
    }
  }
}

Si levelThree est une propriété dynamique, c’est-à-dire pour définir l’une des propriétés dans levelTwo, vous pouvez utiliser [propertyName]: "I am now updated!"propertyName contient le nom de la propriété dans levelTwo.

1
ron4ex

Une autre approche consiste à utiliser la récursivité pour creuser à travers l'objet: 

(function(root){

  function NestedSetterAndGetter(){
    function setValueByArray(obj, parts, value){

      if(!parts){
        throw 'No parts array passed in';
      }

      if(parts.length === 0){
        throw 'parts should never have a length of 0';
      }

      if(parts.length === 1){
        obj[parts[0]] = value;
      } else {
        var next = parts.shift();

        if(!obj[next]){
          obj[next] = {};
        }
        setValueByArray(obj[next], parts, value);
      }
    }

    function getValueByArray(obj, parts, value){

      if(!parts) {
        return null;
      }

      if(parts.length === 1){
        return obj[parts[0]];
      } else {
        var next = parts.shift();

        if(!obj[next]){
          return null;
        }
        return getValueByArray(obj[next], parts, value);
      }
    }

    this.set = function(obj, path, value) {
      setValueByArray(obj, path.split('.'), value);
    };

    this.get = function(obj, path){
      return getValueByArray(obj, path.split('.'));
    };

  }
  root.NestedSetterAndGetter = NestedSetterAndGetter;

})(this);

var setter = new this.NestedSetterAndGetter();

var o = {};
setter.set(o, 'a.b.c', 'Apple');
console.log(o); //=> { a: { b: { c: 'Apple'}}}

var z = { a: { b: { c: { d: 'test' } } } };
setter.set(z, 'a.b.c', {dd: 'zzz'}); 

console.log(JSON.stringify(z)); //=> {"a":{"b":{"c":{"dd":"zzz"}}}}
console.log(JSON.stringify(setter.get(z, 'a.b.c'))); //=> {"dd":"zzz"}
console.log(JSON.stringify(setter.get(z, 'a.b'))); //=> {"c":{"dd":"zzz"}}
1
ed.

JQuery a une méthode d'extension:

https://api.jquery.com/jquery.extend/

il suffit de passer les écrasements en tant qu’objet et les deux fusionnent.

0
Yamcha

Si vous souhaitez qu'une fonction nécessitant des propriétés antérieures existe, vous pouvez utiliser une fonction similaire. Elle renvoie également un indicateur indiquant si elle a réussi à rechercher et à définir la propriété imbriquée.

function set(obj, path, value) {
    var parts = (path || '').split('.');
    // using 'every' so we can return a flag stating whether we managed to set the value.
    return parts.every((p, i) => {
        if (!obj) return false; // cancel early as we havent found a nested prop.
        if (i === parts.length - 1){ // we're at the final part of the path.
            obj[parts[i]] = value;          
        }else{
            obj = obj[parts[i]]; // overwrite the functions reference of the object with the nested one.            
        }   
        return true;        
    });
}
0
C Smith