问题
I have an object that could be any number of levels deep and could have any existing properties. For example:
var obj = {
db: {
mongodb: {
host: 'localhost'
}
}
};
On that I would like to set (or overwrite) properties like so:
set('db.mongodb.user', 'root');
// or:
set('foo.bar', 'baz');
Where the property string can have any depth, and the value can be any type/thing.
Objects and arrays as values don't need to be merged, should the property key already exist.
Previous example would produce following object:
var obj = {
db: {
mongodb: {
host: 'localhost',
user: 'root'
}
},
foo: {
bar: baz
}
};
How can I realize such a function?
回答1:
This function, using the arguments you specified, should add/update the data in the obj
container. Note that you need to keep track of which elements in obj
schema are containers and which are values (strings, ints, etc.) otherwise you will start throwing 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');
回答2:
Lodash has a _.set() method.
_.set(obj, 'db.mongodb.user', 'root');
_.set(obj, 'foo.bar', 'baz');
回答3:
A bit late but here's a non-library, simpler answer:
/**
* 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);
}
This function I made can do exactly what you need and a little more.
let say we want to change the target value that is deeply nested in this object:
let myObj = {
level1: {
level2: {
target: 1
}
}
}
So we would call our function like so:
setDeep(myObj, ["level1", "level2", "target1"], 3);
will result in:
myObj = { level1: { level2: { target: 3 } } }
Setting the set recursively flag to true will set objects if they don't exist.
setDeep(myObj, ["new", "path", "target"], 3);
will result in this:
obj = myObj = {
new: {
path: {
target: 3
}
},
level1: {
level2: {
target: 3
}
}
}
回答4:
Inspired by @bpmason1's answer:
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;
}
Example:
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' } }
回答5:
Lodash has a method called update that does exactly what you need.
This method receives the following parameters:
- The object to update
- The path of the property to update (the property can be deeply nested)
- A function that returns the value to update (given the original value as a parameter)
In your example it would look like this:
_.update(obj, 'db.mongodb.user', function(originalValue) {
return 'root'
})
回答6:
ES6 has a pretty cool way to do this too using Computed Property Name and Rest Parameter.
const obj = {
levelOne: {
levelTwo: {
levelThree: "Set this one!"
}
}
}
const updatedObj = {
...obj,
levelOne: {
...obj.levelOne,
levelTwo: {
...obj.levelOne.levelTwo,
levelThree: "I am now updated!"
}
}
}
If levelThree
is a dynamic property i.e. to set any of the property in levelTwo
, you can use [propertyName]: "I am now updated!"
where propertyName
holds the name of the property in levelTwo
.
回答7:
We can use a recursion function:
/**
* 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)
}
It's more simple!
回答8:
I just write a small function using ES6 + recursion to achieve the goal.
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');
I used it a lot on react to update state, it worked pretty well for me.
回答9:
If you only need to change deeper nested objects, then another method could be to reference the object. As JS objects are handled by their references, you can create a reference to an object you have string-key access to.
Example:
// 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
}
};
回答10:
Another approach is to use recursion to dig through the object:
(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"}}
回答11:
I created gist for setting and getting obj values by string based on correct answer. You can download it or use it as npm/yarn package.
// 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
回答12:
I needed to achieve the same thing, but in Node.js... So, I found this nice module: https://www.npmjs.com/package/nested-property
Example:
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"));
回答13:
If you would like a function that required prior properties to exist, then you could use something like this, it would also return a flag stating whether it managed to find and set the nested property.
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;
});
}
回答14:
Inspired by ClojureScript's assoc-in
(https://github.com/clojure/clojurescript/blob/master/src/main/cljs/cljs/core.cljs#L5280), using recursion:
/**
* 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);
}
Note:
With the provided implementation,
assoc_in({"a": 1}, ["a", "b"], 2)
returns
{"a": 1}
I would prefer that it throw an error in this case. If desired, you can add a check in assoc
to verify m
is either an object or array and throw an error otherwise.
回答15:
JQuery has an extend method:
https://api.jquery.com/jquery.extend/
just pass the overwrites as an object and it will merge the two.
来源:https://stackoverflow.com/questions/18936915/dynamically-set-property-of-nested-object