如何在JavaScript中通过字符串名称设置对象属性(对象属性的属性)?

62

假设我们只知道

var obj = {};
var propName = "foo.bar.foobar";

我们如何将属性obj.foo.bar.foobar设置为特定值(比如"hello world")?假设我们只有以字符串形式给出的属性名称:

obj.foo.bar.foobar = "hello world";

3
JavaScript对象的动态深度设置的副本。 - Cerbrus
1
这可能会帮助那些试图理解这个问题答案的人...http://stackoverflow.com/questions/39060905/how-recursion-takes-place-in-this-code-snippet - Ankur Marwaha
16个回答

92
function assign(obj, prop, value) {
    if (typeof prop === "string")
        prop = prop.split(".");

    if (prop.length > 1) {
        var e = prop.shift();
        assign(obj[e] =
                 Object.prototype.toString.call(obj[e]) === "[object Object]"
                 ? obj[e]
                 : {},
               prop,
               value);
    } else
        obj[prop[0]] = value;
}

var obj = {},
    propName = "foo.bar.foobar";

assign(obj, propName, "Value");

1
是的,当路径尚不存在时,这个似乎也能工作。 - chtenb
为什么要检查prop的类型?你无论如何都会继续函数流程。 - Stephan Bönnemann-Walenta
这是魔法,我修改了它并在我的库中使用。 setDeep(obj, prop, value, returnObj) { if (is.String(prop)) prop = prop.split("."); if (prop.length > 1) { let e = prop.shift(); Craft.setDeep(obj[e] = is.Object(obj[e]) ? obj[e] : {}, prop, value); } else obj[prop[0]] = value; if (returnObj === true) return obj; } - Saul does Code
有人能解释一下递归是如何工作的吗?函数被调用时传入了一个false值,但在返回时却传入了一个true值?简而言之,递归是如何进行的? - Ankur Marwaha
1
这在数组中不起作用。template[0].item[0].format.color - Demodave
显示剩余9条评论

15

我知道这是一个旧的问题,但我只看到了答案中使用自定义函数。
如果您不介意使用库,请看一下lodash_.set_.get函数。


12

由于这个问题的回答似乎都是错误的,因此我将只引用类似问题的正确答案。

function setDeepValue(obj, value, path) {
    if (typeof path === "string") {
        var path = path.split('.');
    }

    if(path.length > 1){
        var p=path.shift();
        if(obj[p]==null || typeof obj[p]!== 'object'){
             obj[p] = {};
        }
        setDeepValue(obj[p], value, path);
    }else{
        obj[path[0]] = value;
    }
}

使用:

var obj = {};
setDeepValue(obj, 'Hello World', 'foo.bar.foobar');

3
嗯,你的回答看起来就像是我的翻版 :) - VisioN
1
如果我使用 foo.bar.foobarfoo.bar2.foobar2 两次调用它会怎样? - FrancescoMM
在这种情况下,当设置 foo.bar2.foobar2 时,它确实重置了 obj.foo。已编辑代码。 哎呀,没看到你建议的编辑,正在修复。 - Cerbrus
真的有区别吗? - Cerbrus
@bla:它是递归的... setDeepValue(obj[p], value, path); - Cerbrus
显示剩余4条评论

5

编辑:我已经创建了一个jsPerf.com测试用例来比较被接受的答案和我的版本。结果发现,我的版本更快,特别是当你深入到很深的时候。

http://jsfiddle.net/9YMm8/

var nestedObjectAssignmentFor = function(obj, propString, value) {
    var propNames = propString.split('.'),
        propLength = propNames.length-1,
        tmpObj = obj;

    for (var i = 0; i <= propLength ; i++) {
        tmpObj = tmpObj[propNames[i]] = i !== propLength ?  {} : value;  
    }
    return obj;
}

var obj = nestedObjectAssignment({},"foo.bar.foobar","hello world");


这似乎在Chrome上稍微慢一些,比建议使用递归函数要慢。但是,在IE/FF上它的速度显著快很多。然而,Chrome拥有最高的迭代次数/秒,因为V8在JavaScript执行方面是一个怪物。 - Cerbrus
@Cerbrus 看起来这取决于浏览器,我自己创建了测试用例 http://jsperf.com/nested-object-assignment - Stephan Bönnemann-Walenta
1
代码性能测试101: 不要在每次迭代中初始化你的函数 - Cerbrus
仅适用于空的第一个参数对象,否则会覆盖原始对象数据。 - art

4

所有的解决方案在设置时都覆盖了原始数据,因此我进行了以下调整,将其改为单个对象:

 var obj = {}
 nestObject.set(obj, "a.b", "foo"); 
 nestObject.get(obj, "a.b"); // returns foo     

 var nestedObject = {
     set: function(obj, propString, value) {
         var propNames = propString.split('.'),
             propLength = propNames.length-1,
             tmpObj = obj;
         for (var i = 0; i <= propLength ; i++) {
             if (i === propLength){
                 if(tmpObj[propNames[i]]){
                     tmpObj[propNames[i]] = value;
                 }else{
                     tmpObj[propNames[i]] = value;
                 }
             }else{
                 if(tmpObj[propNames[i]]){
                     tmpObj = tmpObj[propNames[i]];
                 }else{
                     tmpObj = tmpObj[propNames[i]] = {};
                 }
             }
         }
         return obj;
     },
     get: function(obj, propString){
         var propNames = propString.split('.'),
             propLength = propNames.length-1,
             tmpObj = obj;
         for (var i = 0; i <= propLength ; i++) {
             if(tmpObj[propNames[i]]){
                 tmpObj = tmpObj[propNames[i]];
             }else{
                 break;
             }
         }
         return tmpObj;
     }
 };

也可以将函数更改为Object.prototype方法,将obj参数更改为this:

Object.prototype = { setNested = function(){ ... }, getNested = function(){ ... } } 

{}.setNested('a.c','foo') 

3
这里有一个返回更新后对象的例子。
function deepUpdate(value, path, tree, branch = tree) {
  const last = path.length === 1;
  branch[path[0]] = last ? value : branch[path[0]];
  return last ? tree : deepUpdate(value, path.slice(1), tree, branch[path[0]]);
}

const path = 'cat.dog';
const updated = deepUpdate('a', path.split('.'), {cat: {dog: null}})
// => { cat: {dog: 'a'} }

3
这里有一个使用引用的简单函数可以实现它。
    function setValueByPath (obj, path, value) {
        var ref = obj;

        path.split('.').forEach(function (key, index, arr) {
            ref = ref[key] = index === arr.length - 1 ? value : {};
        });

        return obj;
    }

3

我刚刚从几个线程和一些自定义代码中编译了一个get和set函数。

在设置时,它还将创建不存在的键。

function setValue(object, path, value) {
    var a = path.split('.');
    var o = object;
    for (var i = 0; i < a.length - 1; i++) {
        var n = a[i];
        if (n in o) {
            o = o[n];
        } else {
            o[n] = {};
            o = o[n];
        }
    }
    o[a[a.length - 1]] = value;
}

function getValue(object, path) {
    var o = object;
    path = path.replace(/\[(\w+)\]/g, '.$1');
    path = path.replace(/^\./, '');
    var a = path.split('.');
    while (a.length) {
        var n = a.shift();
        if (n in o) {
            o = o[n];
        } else {
            return;
        }
    }
    return o;
}

1
我喜欢这个方法,因为它也可以处理数组索引,例如 "property[3]"。但是,要使 setValue 方法像这样工作,我们还必须添加 "path = path.replace(/[(\w+)]/g, '.$1');" 到 setValue 方法中。 - Etherman

3

一个非常简单的实现。

这个实现应该是非常高效的。 它避免了递归和函数调用,同时保持了简单性。

/**
 * Set the value of a deep property, creating new objects as necessary.
 * @param {Object} obj The object to set the value on.
 * @param {String|String[]} path The property to set.
 * @param {*} value The value to set.
 * @return {Object} The object at the end of the path.
 * @author github.com/victornpb
 * @see https://dev59.com/GmYr5IYBdhLWcg3wYZSD#46060952
 * @example
 * setDeep(obj, 'foo.bar.baz', 'quux');
 */
function setDeep(obj, path, value) {
    const props = typeof path === 'string' ? path.split('.') : path;
    for (var i = 0, n = props.length - 1; i < n; ++i) {
        obj = obj[props[i]] = obj[props[i]] || {};
    }
    obj[props[i]] = value;
    return obj;
}
  
  

/*********************** EXAMPLE ***********************/

const obj = {
    hello : 'world',
};

setDeep(obj, 'root', true);
setDeep(obj, 'foo.bar.baz', 1);
setDeep(obj, ['foo','quux'], '');

console.log(obj);
// ⬇︎ Click "Run" below to see output


应支持设置深度数组:setDeep(obj, 'foo.bar.baz[0]', 1); - vsync
@vsync 我在数组内设置值,但如果该数组不存在,则假定对象创建,并将索引视为键。 - Vitim.us

3
你可以拆分路径并检查接下来的元素是否存在。如果不存在,则将对象分配给新属性。
然后返回属性的值。
最后,分配该值。

function setValue(object, path, value) {
    var fullPath = path.split('.'),
        way = fullPath.slice(),
        last = way.pop();

    way.reduce(function (r, a) {
        return r[a] = r[a] || {};
    }, object)[last] = value;
}

var object = {},
    propName = 'foo.bar.foobar',
    value = 'hello world';

setValue(object, propName, value);
console.log(object);


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