动态设置嵌套对象的属性

148

我有一个对象,它可能有任意多个层级,并且可能具有任何现有属性。

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

我该如何实现这样的功能?


set('foo', 'bar'); set('foo.baz', 'qux'); 的结果应该是什么?其中 foo 最初保存一个 String,然后变成一个 Object'bar' 会发生什么? - Jonathan Lonowski
2
可能是重复的问题:使用字符串键访问嵌套的JavaScript对象 - Robert Levy
可能这个可以帮到你:https://dev59.com/XHRB5IYBdhLWcg3wLkxM - Rene M.
1
如果你去掉set()方法,只是执行obj.db.mongodb.user = 'root';,那么你似乎想要的就是这样了? - adeneo
@JonathanLonowski Lonowski的barObject覆盖了。 @adeneo和@rmertins确实如此 :) 但不幸的是,我必须包装一些其他逻辑。 @Robert Levy我找到了那个并使其可访问,但设置它似乎更加复杂... - John B.
28个回答

147

使用您指定的参数,此函数应添加/更新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');

2
@bpmason1,您能解释一下为什么您在每个地方都使用了var schema = obj而不是只使用obj吗? - sman591
4
schema是一个指针,通过schema = schema[elem]向下移动路径。所以在for循环后,schema[pList[len - 1]]指向obj中的mongo.db.user - webjay
解决了我的问题,谢谢。在 MDN 文档中找不到这个。但我还有另一个疑问,如果赋值运算符给出内部对象的引用,那么如何从 object1 创建一个单独的 object2,以便对 object2 进行的更改不会反映在 object1 上。 - Onix
1
@Onix,你可以使用lodash的cloneDeep函数来实现这个功能。 - Aakash Thakur
@Onix,那是不可能的。只有在你改变属性时才会发生这种情况。 - Siddharth Shyniben
显示剩余2条评论

127

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

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

2
它能用于设置键的值吗?如果可以,你能分享一个例子吗?谢谢。 - sage poudel
这很棒,但是你如何跟踪/确定路径? - Tom
@aheuermann 我有几个嵌套数组的层级,如果是多层嵌套的对象数组,我该如何设置属性? - Aminul
4
请注意,如果键的一部分包含数字,例如“foo.bar.350350”,那么它将不能按预期工作。相反,它会创建350350个空元素! - daGrevis
如何使lodash在处理数字时不创建数组,而始终生成字符串? - Joseph Astrahan
显示剩余3条评论

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中经常使用它来更新状态,对我来说效果非常好。


2
这很方便,我不得不在propPath上使用toString()来使其与嵌套属性一起工作,但之后它表现得非常好。const [head,...rest] = propPath.toString().split('.')。 - WizardsOfWor
2
@user738048 @Bruno-Joaquim 这行代码 this.updateStateProp(obj[head], value, rest); 应该改为 this.updateStateProp(obj[head], value, rest.join()); - ma.mehralian

25

有点晚了,但这里有一个非库、更简单的答案:

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

将递归设置标志设置为 true 将会在对象不存在时创建对象。

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

会导致这个结果:

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

1
使用这段代码,简洁明了。 我使用了reduce的第三个参数来代替计算level - Juan Lanus
6
我认为level需要+1或path.length-1。 - ThomasReggi
2
当不执行缩减操作时,请勿使用reduce。 - McTrafik
1
一个循环。reduce函数只是一个带有累加器的for循环的语法糖,适用于缩减操作。请参考类似这样的内容:https://medium.com/winnintech/implementing-reduce-in-javascript-part-1-7ea8711e194,而且这段代码并没有累加任何东西,也没有执行缩减操作,因此在此处调用reduce是模式的误用。 - McTrafik
1
不应该在具有副作用(意图为纯函数)的情况下使用“reduce”。一个好的经验法则是,如果没有使用“map”,“filter”或“reduce”的返回值,则应该将函数本身替换为“forEach”。 - Kendall
显示剩余2条评论

23

我们可以使用递归函数:

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

更加简单易懂!


3
看起来不错!只需要检查obj参数以确保它不是虚假值,如果链下的任何属性不存在,就会引发错误。 - C Smith
2
你可以只使用 path.slice(1)。 - Marcos Pereira
1
非常好的答案,一个简洁明了的解决方案。 - chim
只需要添加 -> if(!obj) return - Sunil Garg
这假设嵌套的键已经存在。 - Nermin
显示剩余2条评论

12

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


这个没有处理像@bpmason1的数组索引。 - jolly

11

我使用纯 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);


你可以通过声明“obj”并设置默认值来消除第一个if块,setNestedProp =(obj = {},keys,value)=> { - blindChicken
1
不错。回头看,也可以在原地解构键参数,这样可以再节省一行代码。 - Henry Ing-Simmons
基本上现在只是一行代码 - Henry Ing-Simmons

10

ES6也有一种非常酷的方法来实现这一点,即使用计算属性名Rest参数

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]: "我现在已更新!",其中propertyName保存了levelTwo中属性的名称。


9
Lodash有一个名为update的方法,可以满足你的需求。
该方法接收以下参数:
  1. 要更新的对象
  2. 要更新的属性路径(属性可以是深度嵌套的)
  3. 返回要更新值的函数(以原始值作为参数)
在你的示例中,它将如下所示:
_.update(obj, 'db.mongodb.user', function(originalValue) {
  return 'root'
})

3

我需要在Node.js中实现相同的功能,于是我找到了这个好用的模块: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"));

如何解决复杂嵌套对象的问题。const x = { 'one': 1, 'two': 2, 'three': { 'one': 1, 'two': 2, 'three': [ { 'one': 1 }, { 'one': 'ONE' }, { 'one': 'I' } ] }, 'four': [0, 1, 2] }; console.log(np.get(x, 'three.three[0].one')); - Sumukha H S

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接