How to set object property (of object property of..) given its string name in JavaScript?

Suppose we are only given

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

How can we set the property obj.foo.bar.foobar to a certain value (say "hello world")? So I want to achieve this, while we only have the property name in a string:

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

Answers:

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

Since this question appears to be answered by incorrect answers, I'll just refer to the correct answer from a similar question

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

Use:

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

edit: I've created a jsPerf.com testcase to compare the accepted answer with my version. Turns out that my version is faster, especially when you go very deep.

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

?

?

Answer

All solutions overid any of the original data when setting so I have tweaked with the following, made it into a single object too:

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

Can also change functions to be an Oject.prototype method changing obj param to this:

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

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

I know it's an old one, but I see only custom functions in answers.
If you don't mind using a library, look at lodash _.set and _.get function.

Answer

Here is a simple function to do that using reference.

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

Here's one that returns the updated object

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

Here is a get and set function i just compiled from a couple of threads + some custom code.

It will also create keys that don't exist on 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;
}
Answer

A very straightforward one.

No recursions or callbacks overhead.

function setDeepVal(obj, path, val) {
  var props = path.split('.');
  for (var i = 0, n = props.length - 1; i < n; ++i) {
    obj = obj[props[i]] = obj[props[i]] || {};
  }
  obj[props[i]] = val;
  return obj;
}



// TEST
var obj = { hello : 'world' };
setDeepVal(obj, 'foo.bar.baz', 1);
setDeepVal(obj, 'foo.bar2.baz2', 2);
console.log(obj);

Answer

You could split the path and make a check if the following element exist. If not assign an object to the new property.

Return then the value of the property.

At the end assign the value.

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

Answer

I was looking for an answer that does not overwrite existing values and was easily readable and was able to come up with this. Leaving this here in case it helps others with the same needs

function setValueAtObjectPath(obj, pathString, newValue) {
  // create an array (pathComponents) of the period-separated path components from pathString
  var pathComponents = pathString.split('.');
  // create a object (tmpObj) that references the memory of obj
  var tmpObj = obj;

  for (var i = 0; i < pathComponents.length; i++) {
    // if not on the last path component, then set the tmpObj as the value at this pathComponent
    if (i !== pathComponents.length-1) {
      // set tmpObj[pathComponents[i]] equal to an object of it's own value
      tmpObj[pathComponents[i]] = {...tmpObj[pathComponents[i]]}
      // set tmpObj to reference tmpObj[pathComponents[i]]
      tmpObj = tmpObj[pathComponents[i]]
    // else (IS the last path component), then set the value at this pathComponent equal to newValue 
    } else {
      // set tmpObj[pathComponents[i]] equal to newValue
      tmpObj[pathComponents[i]] = newValue
    }
  }
  // return your object
  return obj
}
Answer

Same as Rbar's answers, very useful when you're working with redux reducers. I use lodash clone instead of spread operator to support arrays too:

export function cloneAndPatch(obj, path, newValue, separator='.') {
    let stack = Array.isArray(path) ? path : path.split(separator);
    let newObj = _.clone(obj);

    obj = newObj;

    while (stack.length > 1) {
        let property = stack.shift();
        let sub = _.clone(obj[property]);

        obj[property] = sub;
        obj = sub;
    }

    obj[stack.shift()] = newValue;

    return newObj;
}
Answer
Object.getPath = function(o, s) {
    s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
    s = s.replace(/^\./, '');           // strip a leading dot
    var a = s.split('.');
    for (var i = 0, n = a.length; i < n; ++i) {
        var k = a[i];
        if (k in o) {
            o = o[k];
        } else {
            return;
        }
    }
    return o;
};

Object.setPath = function(o, p, v) {
    var a = p.split('.');
    var o = o;
    for (var i = 0; i < a.length - 1; i++) {
        if (a[i].indexOf('[') === -1) {
            var n = a[i];
            if (n in o) {
                o = o[n];
            } else {
                o[n] = {};
                o = o[n];
            }
        } else {
            // Not totaly optimised
            var ix = a[i].match(/\[.*?\]/g)[0];
            var n = a[i].replace(ix, '');
            o = o[n][ix.substr(1,ix.length-2)]
        }
    }

    if (a[a.length - 1].indexOf('[') === -1) {
        o[a[a.length - 1]] = v;
    } else {
        var ix = a[a.length - 1].match(/\[.*?\]/g)[0];
        var n = a[a.length - 1].replace(ix, '');
        o[n][ix.substr(1,ix.length-2)] = v;
    }
};
Answer

Here's a simple method that uses a scoped Object that recursively set's the correct prop by path.

function setObjectValueByPath(pathScope, value, obj) {
  const pathStrings = pathScope.split('/');
  obj[pathStrings[0]] = pathStrings.length > 1 ?
    setObjectValueByPath(
      pathStrings.splice(1, pathStrings.length).join('/'),
      value,
      obj[pathStrings[0]]
    ) :
    value;
  return obj;
}

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.