Propiedad establecida dinámicamente del objeto anidado

84

Tengo un objeto que podría tener cualquier cantidad de niveles de profundidad y podría tener propiedades existentes. Por ejemplo:

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

En eso, me gustaría establecer (o sobrescribir) propiedades así:

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

Donde la cadena de propiedad puede tener cualquier profundidad y el valor puede ser de cualquier tipo / cosa.
Los objetos y matrices como valores no necesitan fusionarse, si la clave de propiedad ya existe.

El ejemplo anterior produciría el siguiente objeto:

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

¿Cómo puedo realizar tal función?

John B.
fuente
¿Cuál debería ser el resultado set('foo', 'bar'); set('foo.baz', 'qux');, dónde fooprimero se mantiene un y Stringluego se convierte en un Object? ¿Qué pasa con 'bar'?
Jonathan Lonowski
podría ser posible esta ayuda: stackoverflow.com/questions/695050/…
Rene M.
1
Si elimina el set()método y simplemente obj.db.mongodb.user = 'root';tiene exactamente lo que parece querer?
Adeneo
@JonathanLonowski Lonowski barse sobrescribe con el Object. @adeneo y @rmertins De hecho :) Pero desafortunadamente tengo que envolver otra lógica. @Robert Levy Encontré ese y conseguí que el acceso funcionara, pero configurarlo parece mucho más complicado ...
John B.

Respuestas:

92

Esta función, usando los argumentos que especificó, debería agregar / actualizar los datos en el objcontenedor. Tenga en cuenta que debe realizar un seguimiento de qué elementos del objesquema son contenedores y cuáles son valores (cadenas, entradas, etc.); de lo contrario, comenzará a generar excepciones.

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');
bpmason1
fuente
2
@ bpmason1, ¿podría explicar por qué usó en var schema = objlugar de solo en objtodas partes?
sman591
3
@ sman591 schemaes un puntero que se mueve por la ruta con schema = schema[elem]. Entonces, después del ciclo for, schema[pList[len - 1]]apunta a mongo.db.user en obj.
webjay
eso resolvió mi problema gracias, no pude encontrar esto en MDN docs. Pero tengo otra duda, si el operador de asignación da una referencia a los objetos internos, entonces cómo hacer un objeto2 separado del objeto1 para que los cambios realizados en el objeto2 no se reflejen en el objeto1.
Onix
@Onix Puede hacer uso de la cloneDeepfunción lodash para esto.
Aakash Thakur
@Onix const clone = JSON.parse (JSON.stringify (obj))
Mike Makuch
72

Lodash tiene un método _.set () .

_.set(obj, 'db.mongodb.user', 'root');
_.set(obj, 'foo.bar', 'baz');
Aheuermann
fuente
1
¿Se puede usar también para establecer el valor de la clave? si es así, puede compartir un ejemplo. Gracias
poudel de salvia
Esto es genial, pero ¿cómo realizaría un seguimiento o determinaría el camino?
Tom
19

Un poco tarde, pero aquí hay una respuesta más simple que no es de biblioteca:

/**
 * 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) {
    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;
        } 
        return a[b];
    }, obj);
}

Esta función que hice puede hacer exactamente lo que necesitas y un poco más.

digamos que queremos cambiar el valor objetivo que está profundamente anidado en este objeto:

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

Entonces llamaríamos a nuestra función así:

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

resultará en:

myObj = {level1: {level2: {target: 3}}}

Establecer el indicador de conjunto recursivamente en verdadero establecerá objetos si no existen.

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

resultará en esto:

obj = myObj = {
    new: {
         path: {
             target: 3
         }
    },
    level1: {
        level2: {
           target: 3
       }
    }
}
Philll_t
fuente
1
Usé este código, limpio y simple. En lugar de calcular level, utilicé reduceel tercer argumento.
Juan Lanus
Creo que leveldebe ser +1 o path.length-1
ThomasReggi
12

Podemos usar una función de recursividad:

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

¡Es más simple!

Hemã Vidal
fuente
3
¡se ve bien! solo necesita verificar el parámetro obj para asegurarse de que no sea falso, arrojará un error si alguno de los accesorios de la cadena no existe.
C Smith
2
puedes usar path.slice (1);
Marcos Pereira
1
Excelente respuesta, una solución agradable y concisa.
Chim
Creo en esa declaración if, debería haber sido obj[path[0]] = value;porque pathsiempre es de tipo string[], incluso cuando solo queda 1 cadena.
Valorad
Los objetos de Javascript deberían funcionar usando obj[['a']] = 'new value'. Verifique el código: jsfiddle.net/upsdne03
Hemã Vidal
11

Solo escribo una pequeña función usando ES6 + recursividad para lograr el objetivo.

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

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

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

Lo usé mucho para reaccionar para actualizar el estado, funcionó bastante bien para mí.

Bruno Joaquim
fuente
1
esto fue útil, tuve que poner un toString () en proPath para que funcione con propiedades anidadas, pero después de eso funcionó muy bien. const [cabeza, ... resto] = ruta prop. a cadena (). dividir ('.');
WizardsOfWor
1
@ user738048 @ Bruno-Joaquim la línea this.updateStateProp(obj[head], value, rest);debería serthis.updateStateProp(obj[head], value, rest.join());
ma.mehralian
8

ES6 también tiene una manera genial de hacer esto usando el nombre de propiedad calculado y el parámetro de descanso .

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

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

Si levelThreees una propiedad dinámica, es decir, para establecer cualquiera de las propiedades levelTwo, puede usar [propertyName]: "I am now updated!"where propertyNamecontiene el nombre de la propiedad levelTwo.

ron4ex
fuente
7

Lodash tiene un método llamado actualización que hace exactamente lo que necesita.

Este método recibe los siguientes parámetros:

  1. El objeto a actualizar
  2. La ruta de la propiedad para actualizar (la propiedad puede estar profundamente anidada)
  3. Una función que devuelve el valor para actualizar (dado el valor original como parámetro)

En su ejemplo, se vería así:

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

Inspirado por la respuesta 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;
}

Ejemplo:

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' } }
webjay
fuente
2

Creé lo esencial para establecer y obtener valores obj por cadena según la respuesta correcta. Puede descargarlo o usarlo como paquete 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
Chiffie
fuente
1

Si solo necesita cambiar objetos anidados más profundos, otro método podría ser hacer referencia al objeto. Como los objetos JS son manejados por sus referencias, puede crear una referencia a un objeto al que tiene acceso de clave de cadena.

Ejemplo:

// 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
    }
};
agregado1166877
fuente
1

Otro enfoque es usar la recursividad para excavar en el objeto:

(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"}}
ed.
fuente
1

Necesitaba lograr lo mismo, pero en Node.js ... Entonces, encontré este bonito módulo: https://www.npmjs.com/package/nested-property

Ejemplo:

var mod = require("nested-property");
var obj = {
  a: {
    b: {
      c: {
        d: 5
      }
    }
  }
};
console.log(mod.get(obj, "a.b.c.d"));
mod.set(obj, "a.b.c.d", 6);
console.log(mod.get(obj, "a.b.c.d"));
Rehmat
fuente
cómo resolver objetos anidados complejos. `` const x = {'uno': 1, 'dos': 2, 'tres': {'uno': 1, 'dos': 2, 'tres': [{'uno': 1}, { 'uno': 'UNO'}, {'uno': 'I'}]}, 'cuatro': [0, 1, 2]}; console.log (np.get (x, 'tres.tres [0] .uno')); ``
Sumukha HS
1

Se me ocurrió mi propia solución usando es6 puro y recursividad que no mute el objeto original.

const setNestedProp = (obj = {}, [first, ...rest] , value) => ({
  ...obj,
  [first]: rest.length
    ? setNestedProp(obj[first], rest, value)
    : value
});

const result = setNestedProp({}, ["first", "second", "a"], 
"foo");
const result2 = setNestedProp(result, ["first", "second", "b"], "bar");

console.log(result);
console.log(result2);

Henry Ing-Simmons
fuente
Puede eliminar el primer bloque if declarando "obj" con un valor predeterminado setNestedProp = (obj = {}, keys, value) => {
blindChicken
1
Qué bueno. Mirar hacia atrás también podría desestructurar el argumento de las claves in situ y guardar otra línea de código
Henry Ing-Simmons
Básicamente, una línea ahora 👍
Henry Ing-Simmons
0

Si desea que exista una función que requiera que existan propiedades anteriores, entonces podría usar algo como esto, también devolvería una bandera que indica si logró encontrar y establecer la propiedad anidada.

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;        
    });
}
C Smith
fuente
0

Inspirado en ClojureScript assoc-in( https://github.com/clojure/clojurescript/blob/master/src/main/cljs/cljs/core.cljs#L5280 ), usando recursividad:

/**
 * Associate value (v) in object/array (m) at key/index (k).
 * If m is falsy, use new object.
 * Returns the updated object/array.
 */
function assoc(m, k, v) {
    m = (m || {});
    m[k] = v;
    return m;
}

/**
 * Associate value (v) in nested object/array (m) using sequence of keys (ks)
 * to identify the path to the nested key/index.
 * If one of the values in the nested object/array doesn't exist, it adds
 * a new object.
 */
function assoc_in(m={}, [k, ...ks], v) {
    return ks.length ? assoc(m, k, assoc_in(m[k], ks, v)) : assoc(m, k, v);
}

/**
 * Associate value (v) in nested object/array (m) using key string notation (s)
 * (e.g. "k1.k2").
 */
function set(m, s, v) {
    ks = s.split(".");
    return assoc_in(m, ks, v);
}

Nota:

Con la implementación proporcionada,

assoc_in({"a": 1}, ["a", "b"], 2) 

devoluciones

{"a": 1}

Preferiría que arrojara un error en este caso. Si lo desea, puede agregar un registro assocpara verificar mque sea un objeto o una matriz y, de lo contrario, arrojar un error.

Lucas Leblow
fuente
0

Intenté escribir este método de conjunto en breve , ¡puede ayudar a alguien!

function set(obj, key, value) {
 let keys = key.split('.');
 if(keys.length<2){ obj[key] = value; return obj; }

 let lastKey = keys.pop();

 let fun = `obj.${keys.join('.')} = {${lastKey}: '${value}'};`;
 return new Function(fun)();
}

var obj = {
"hello": {
    "world": "test"
}
};

set(obj, "hello.world", 'test updated'); 
console.log(obj);

set(obj, "hello.world.again", 'hello again'); 
console.log(obj);

set(obj, "hello.world.again.onece_again", 'hello once again');
console.log(obj);

Arun Saini
fuente