动态设置嵌套对象的属性

IT技术 javascript ecmascript-5
2021-01-23 14:17:31

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

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

我怎样才能实现这样的功能?

6个回答

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

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');
@Onix const clone = JSON.parse(JSON.stringify(obj))
2021-03-18 14:17:31
@sman591schema是一个指针,它沿着路径向下移动schema = schema[elem]所以在 for 循环之后,schema[pList[len - 1]]指向 mongo.db.user in obj
2021-03-20 14:17:31
解决了我的问题,谢谢,在 MDN 文档中找不到这个。但我有另一个疑问,如果赋值运算符提供对内部对象的引用,那么如何从 object1 中创建一个单独的 object2,以便对 object2 所做的更改不会反映在 object1 上。
2021-03-20 14:17:31
@Onix 您可以cloneDeep为此使用 lodash功能。
2021-03-29 14:17:31
@bpmason1 你能解释一下为什么你使用var schema = obj而不是obj到处使用吗?
2021-04-03 14:17:31

Lodash 有一个_.set()方法。

_.set(obj, 'db.mongodb.user', 'root');
_.set(obj, 'foo.bar', 'baz');
它也可以用于设置键的值吗?如果是,你能分享一个例子吗?谢谢
2021-03-22 14:17:31
@aheuermann 我有多个级别的嵌套数组,如何在对象的多级嵌套数组的情况下设置属性
2021-03-29 14:17:31
请注意,当密钥的一部分包含诸如“foo.bar.350350”之类的数字时,这将无法按预期工作。它将创建 350350 个空元素!
2021-03-29 14:17:31
lodash set 也接受路径数组,例如 _.set(obj, ['db', 'mongodb', 'user'], 'root');
2021-04-05 14:17:31
这很好,但是您将如何跟踪/确定路径?
2021-04-09 14:17:31

我只是用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');

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

这很方便,我不得不在 proPath 上放置一个 toString() 以使其与嵌套属性一起工作,但之后它工作得很好。const [head, ...rest] = propPath.toString().split('.');
2021-03-15 14:17:31
@user738048 @Bruno-Joaquim 这条线this.updateStateProp(obj[head], value, rest);应该是this.updateStateProp(obj[head], value, rest.join());
2021-03-23 14:17:31

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

/**
 * 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 = { level1: { level2: { target: 3 } } }

如果对象不存在,将 set recursively 标志设置为 true 将设置对象。

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

将导致:

obj = myObj = {
    new: {
         path: {
             target: 3
         }
    },
    level1: {
        level2: {
           target: 3
       }
    }
}
使用了这段代码,干净简单。level我使用了reduce第三个参数,而不是计算
2021-03-17 14:17:31
我相信这level需要是 +1 或path.length-1
2021-03-19 14:17:31
@McTrafik 你应该用什么代替
2021-03-20 14:17:31
不执行归约时不应使用归约。
2021-03-23 14:17:31
一个循环。reduce 函数只是 for 循环的语法糖,带有适用于减少的累加器。看到这样的东西:medium.com/winnintech /... 这段代码不会累积任何东西,也不会执行减少,所以这里的reduce调用是对模式的误用。
2021-03-25 14:17:31

我们可以使用递归函数:

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

更简单!

我相信 if 语句,它应该是obj[path[0]] = value;因为path总是 type string[],即使只剩下 1 个字符串。
2021-03-13 14:17:31
看起来挺好的!只需要检查 obj 参数以确保它不是错误的,如果链中的任何道具不存在,则会抛出错误。
2021-03-22 14:17:31
Javascript 对象应该使用obj[['a']] = 'new value'. 检查代码:jsfiddle.net/upsdne03
2021-03-22 14:17:31
优秀的答案,一个漂亮而简洁的解决方案。
2021-03-23 14:17:31
你可以只使用 path.slice(1);
2021-04-05 14:17:31