动态设置嵌套对象的属性

我有一个对象,可以是任意数量的层次深度,可以有任何现有的属性。 例如:

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

关于这一点,我想像下面这样设置(或覆盖)属性:

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

其中属性字符串可以有任何深度,值可以是任何类型/事物。
如果属性键已经存在,则不需要合并作为值的对象和数组。

前面的例子将产生以下对象:

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

我如何实现这样的功能?

129786 次浏览

这个函数使用您指定的参数,应该添加/更新 obj容器中的数据。注意,您需要跟踪 obj模式中哪些元素是容器,哪些是值(字符串、 int 等) ,否则您将开始抛出异常。

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

如果您只需要更改更深的嵌套对象,那么另一种方法可以是引用该对象。由于 JS 对象是通过它们的引用来处理的,因此您可以创建对具有字符串键访问权限的对象的引用。

例如:

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

另一种方法是使用递归挖掘对象:

(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"}}

Lodash 有一个名为 更新的方法,它可以完全满足您的需要。

此方法接收下列参数:

  1. 要更新的对象
  2. 要更新的属性的路径(该属性可以深度嵌套)
  3. 返回要更新的值的函数(给定原始值作为参数)

在你的例子中,它看起来是这样的:

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

Lodash 有一个 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _方法。

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

有点晚了,但这里有一个非图书馆的,更简单的答案:

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

我做的这个函数可以完全满足你的需要,甚至更多。

假设我们想要更改嵌套在这个对象中的目标值:

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

我们可以这样调用函数:

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

会导致:

MyObj = { 等级1: { 等级2: { 目标: 3 } } }

递归地将 set 标志设置为 true 将设置不存在的对象。

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

会导致:

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

受@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;
}

例如:

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' } }

我们可以使用递归函数:

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

这样更简单!

ES6使用 计算机属性名称休息参数也有一个非常酷的方法来做到这一点。

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


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

如果 levelThree是一个动态属性,即设置 levelTwo中的任何属性,则可以使用 [propertyName]: "I am now updated!",其中 propertyName保存 levelTwo中的属性的名称。

我只是用 ES6 + 递归编写了一个小函数来实现这个目标。

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

我经常用它来更新状态,对我来说效果很好。

我创建了 大意,用于根据正确答案通过字符串设置和获取 obj 值。您可以下载它或使用它作为 npm/纱线包。

// 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

如果您希望函数需要先前的属性存在,那么您可以使用类似这样的东西,它还会返回一个标志,说明它是否设法找到并设置了嵌套属性。

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

JQuery 有一个扩展方法:

Https://api.jquery.com/jquery.extend/

只需将覆盖作为对象传递,它就会将两者合并。

我需要实现同样的目标,但在 Node.js 中..。 因此,我找到了这个不错的模块: < a href = “ https://www.npmjs.com/package/nested-property”rel = “ nofollow norefrer”> https://www.npmjs.com/package/nested-property

例如:

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

灵感来自 ClojureScript 的 assoc-in(https://github.com/clojure/clojurescript/blob/master/src/main/cljs/cljs/core.cljs#L5280) ,使用递归:

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

注:

根据提供的实现,

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

报税表

{"a": 1}

在这种情况下,我希望它抛出一个错误。如果需要,可以在 assoc中添加检查以验证 m是对象还是数组,否则抛出错误。

我尝试写这个 简而言之,集合法,它可能会有帮助的人!

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

我提出了自己的解决方案,使用纯 es6和递归,不会改变原始对象。

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

const set = (o, path, value) => {
const props = path.split('.');
const prop = props.shift()
if (props.length === 0) {
o[prop] = value
} else {
o[prop] = o[prop] ?? {}
set(o[prop], props.join('.'), value)
}
}

这是一个普通的 js 函数,它接受一个路径作为参数,并返回修改后的对象/json

let orig_json = {
string: "Hi",
number: 0,
boolean: false,
object: {
subString: "Hello",
subNumber: 1,
subBoolean: true,
subObject: {
subSubString: "Hello World"
},
subArray: ["-1", "-2", "-3"]
},
array: ["1", "2", "3"]
}


function changeValue(obj_path, value, json) {
let keys = obj_path.split(".")
let obj = { ...json },
tmpobj = {},
prevobj = {}
for (let x = keys.length - 1; x >= 0; x--) {
if (x == 0) {
obj[keys[0]] = tmpobj
} else {
let toeval = 'json.' + keys.slice(0, x).join('.');
prevobj = { ...tmpobj
}
tmpobj = eval(toeval);
if (x == keys.length - 1) tmpobj[keys[x]] = value
else {
tmpobj[keys[x]] = prevobj
}
}
}
return obj
}


let newjson = changeValue("object.subObject.subSubString", "Goodbye world", orig_json);
console.log(newjson)

添加或重写属性的另一种解决方案:

function propertySetter(property, value) {
const sampleObject = {
string: "Hi",
number: 0,
boolean: false,
object: {
subString: "Hello",
subNumber: 1,
subBoolean: true,
subObject: {
subSubString: "Hello World",
},
subArray: ["-1", "-2", "-3"],
},
array: ["1", "2", "3"],
};


const keys = property.split(".");
const propertyName = keys.pop();
let propertyParent = sampleObject;
while (keys.length > 0) {
const key = keys.shift();
if (!(key in propertyParent)) {
propertyParent[key] = {};
}
propertyParent = propertyParent[key];
}
propertyParent[propertyName] = value;
return sampleObject;
}


console.log(propertySetter("object.subObject.anotherSubString", "Hello you"));


console.log(propertySetter("object.subObject.subSubString", "Hello Earth"));


console.log(propertySetter("object.subObject.nextSubString.subSubSubString", "Helloooo"));

扩展 @ bpmason1提供的可接受的答案,以支持字符串路径中的数组,例如字符串路径可以是 'db.mongodb.users[0].name''db.mongodb.users[1].name'

它将设置属性值,如果属性值不存在,将创建属性值。

var obj = {};


function set(path, value) {
var schema = obj;
var keysList = path.split('.');
var len = keysList.length;
for (var i = 0; i < len - 1; i++) {
var key = keysList[i];
// checking if key represents an array element e.g. users[0]
if (key.includes('[')) {
//getting propertyName 'users' form key 'users[0]'
var propertyName = key.substr(0, key.length - key.substr(key.indexOf("["), key.length - key.indexOf("[")).length);
if (!schema[propertyName]) {
schema[propertyName] = [];
}
// schema['users'][getting index 0 from 'users[0]']
if (!schema[propertyName][parseInt(key.substr(key.indexOf("[") + 1, key.indexOf("]") - key.indexOf("[") - 1))]) {
// if it doesn't exist create and initialise it
schema = schema[propertyName][parseInt(key.substr(key.indexOf("[") + 1, key.indexOf("]") - key.indexOf("[") - 1))] = {};
} else {
schema = schema[propertyName][parseInt(key.substr(key.indexOf("[") + 1, key.indexOf("]") - key.indexOf("[") - 1))];
}
continue;
}
if (!schema[key]) {
schema[key] = {};
}
schema = schema[key];
} //loop ends
// if last key is array element
if (keysList[len - 1].includes('[')) {
//getting propertyName 'users' form key 'users[0]'
var propertyName = keysList[len - 1].substr(0, keysList[len - 1].length - keysList[len - 1].substr(keysList[len - 1].indexOf("["), keysList[len - 1].length - keysList[len - 1].indexOf("[")).length);
if (!schema[propertyName]) {
schema[propertyName] = [];
}
// schema[users][0] = value;
schema[propertyName][parseInt(keysList[len - 1].substr(keysList[len - 1].indexOf("[") + 1, keysList[len - 1].indexOf("]") - keysList[len - 1].indexOf("[") - 1))] = value;
} else {
schema[keysList[len - 1]] = value;
}
}


// will create if not exist
set("mongo.db.users[0].name.firstname", "hii0");
set("mongo.db.users[1].name.firstname", "hii1");
set("mongo.db.users[2].name", {
"firstname": "hii2"
});
set("mongo.db.other", "xx");
console.log(obj);


// will set if exist
set("mongo.db.other", "yy");
console.log(obj);

灵感来自 ImmutableJSsetIn 方法,该方法永远不会变异原始。 这适用于混合数组和对象嵌套值。

function setIn(obj = {}, [prop, ...rest], value) {
const newObj = Array.isArray(obj) ? [...obj] : {...obj};
newObj[prop] = rest.length ? setIn(obj[prop], rest, value) : value;
return newObj;
}


var obj = {
a: {
b: {
c: [
{d: 5}
]
}
}
};


const newObj = setIn(obj, ["a", "b", "c", 0, "x"], "new");


//obj === {a: {b: {c: [{d: 5}]}}}
//newObj === {a: {b: {c: [{d: 5, x: "new"}]}}}

以防您想要深度更新或插入对象 试试这个:-

 let init = {
abc: {
c: {1: 2, 3: 5, 0: {l: 3}},
d: 100
}
}
Object.prototype.deepUpdate = function(update){
let key = Object.keys(update);
key.forEach((k) => {
if(typeof update[key] == "object"){
this[k].deepUpdate(update[key], this[k])
}
else
this[k] = update[k]
})
}


init.deepUpdate({abc: {c: {l: 10}}})
console.log(init)

但要确保它会改变原来的对象,你可以使它不改变原来的对象:

JSON.parse(JSON.stringify(init)).deepUpdate({abc: {c: {l: 10}}})

这是一个使用 ES 12的解决方案

function set(obj = {}, key, val) {
const keys = key.split('.')
const last = keys.pop()
keys.reduce((o, k) => o[k] ??= {}, obj)[last] = val
}

(对于旧版本的 javascript,可以在 reduce 中执行 o[k] || o[k] = {})

首先,我们将 keys设置为包含除最后一个键之外的所有内容的数组。

然后在还原过程中,累加器深入到 obj的一个层次 每次,如果没有定义该键的值,则将其初始化为空对象。

最后,我们将最后一个键的值设置为 val

改进 bpmason1的答案: - 添加一个 get ()函数。 - 不需要定义全局存储对象 - 可从同一域名 iFrame 访问

function set(path, value)
{
var schema = parent.document;
path="data."+path;
var pList = path.split('.');
var len = pList.length;
for(var i = 0; i < len-1; i++)
{
if(!schema[pList[i]])
schema[pList[i]] = {}
schema = schema[pList[i]];
}
schema[pList[len-1]] = value;
}


function get(path)
{
path="data."+path;
var schema=parent.document;
var pList = path.split('.');
for(var i = 0; i < pList.length; i++)
schema = schema[pList[i]];
return schema;
}


set('mongo.db.user', 'root');
set('mongo.db.name', 'glen');


console.log(get('mongo.db.name'));  //prints 'glen'

如@aheuermann sed 所示,您可以使用 ABC1呼叫 set库,

但是,如果由于某种原因不想将 lodash添加到项目中,可以使用一个递归函数来设置/重写对象中的值。

/**
* recursion function that called in main function
* @param obj initial JSON
* @param keysList array of keys
* @param value value that you want to set
* @returns final JSON
*/
function recursionSet(obj, keysList, value) {
const key = keysList[0]
if (keysList.length === 1) return { ...obj, [key]: value }
return { ...obj, [key]: (recursionSet(obj?.[key] || {}, keysList.slice(1), value)) }
}


/**
* main function that you can call for set a value in an object by nested keys
* @param obj initial JSON
* @param keysString nested keys that seprated by "."
* @param value value that you want to set
* @returns final JSON
*/
function objectSet(obj, keysString, value) {
return recursionSet(obj, keysString.split('.'), value)
}


// simple usage
const a1 = {}
console.log('simple usage:', objectSet(a1, "b.c.d", 5))


// keep the initial data
const a2 = {b:{e: 8}}
console.log('keep the initial data:', objectSet(a2, "b.c.d", 5))


// override data
const a3 = {b:{e: 8, c:2}}
console.log('override data:', objectSet(a3, "b.c.d", 5))


// complex value
const a4 = {b:{e: 8, c:2}}
console.log('complex value:', objectSet(a4, "b.c.d", {f:12}))

有时如果键也有点(。)它的字符串,这可能会造成一个问题。因为即使是那个单一的键现在也会被分成不同的键。

最好将键路径存储在一个数组中,如下所示: [‘ db’、‘ monGodb’、‘ user’]并使用下面的函数动态分配值。

function set(obj, path, value) {
var schema = obj;
var pList = path.slice();
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;
}
    

let path = ['db','mongodb','user'];
set(obj, path, 'root');

我想就这个有趣的话题留下我的答案。创建为对象设置动态属性的函数可能很困难。

const entity = {
haveDogs: true,
dogs: ['Maya', 'Perla']
}


function isObject(obj) {
return obj instanceof Object && obj.constructor === Object;
}


function setSchema(key, schema, value) {
if (!isObject(value)) {
schema[key] = value;
return
}
      

if (!schema[key]) schema[key] = {}
schema[key] = mutate(schema[key], value);
}


function mutate(obj, newObjData) {
const keys = Object.keys(newObjData)
    

for (const key of keys) {
let schema = obj
const list = key.split('.')
const value = newObjData[key]
const total = list.length - 1
      

if (list.length === 1) {
setSchema(key, schema, value)
continue
}
      

for (let i = 0; i < total; i++) {
const elem = list[i];
if (!schema[elem]) schema[elem] = {}
schema = schema[elem]
}
      

const subField = list[total]
setSchema(subField, schema, value)
}


return obj
}


mutate(entity, {
haveDogs: false,
'pet1.pet2.pet3.pet4.pet5': 'pets',
'bestFriends.list': ['Maya', 'Lucas'],
friends: {
'whitelist.permitted': ['Maya', 'Perla'],
'party.blocked': ['Juan', 'Trump']
}
})


console.log('[entity]', entity)