Javascript: how to dynamically create nested objects using object names given by an array

107,979

Solution 1

function assign(obj, keyPath, value) {
   lastKeyIndex = keyPath.length-1;
   for (var i = 0; i < lastKeyIndex; ++ i) {
     key = keyPath[i];
     if (!(key in obj)){
       obj[key] = {}
     }
     obj = obj[key];
   }
   obj[keyPath[lastKeyIndex]] = value;
}

Usage:

var settings = {};
assign(settings, ['Modules', 'Video', 'Plugin'], 'JWPlayer');

Solution 2

Put in a function, short and fast (no recursion).

var createNestedObject = function( base, names ) {
    for( var i = 0; i < names.length; i++ ) {
        base = base[ names[i] ] = base[ names[i] ] || {};
    }
};

// Usage:
createNestedObject( window, ["shapes", "triangle", "points"] );
// Now window.shapes.triangle.points is an empty object, ready to be used.

It skips already existing parts of the hierarchy. Useful if you are not sure whether the hierarchy was already created.

Or:

A fancier version where you can directly assign the value to the last object in the hierarchy, and you can chain function calls because it returns the last object.

// Function: createNestedObject( base, names[, value] )
//   base: the object on which to create the hierarchy
//   names: an array of strings contaning the names of the objects
//   value (optional): if given, will be the last object in the hierarchy
// Returns: the last object in the hierarchy
var createNestedObject = function( base, names, value ) {
    // If a value is given, remove the last name and keep it for later:
    var lastName = arguments.length === 3 ? names.pop() : false;

    // Walk the hierarchy, creating new objects where needed.
    // If the lastName was removed, then the last object is not set yet:
    for( var i = 0; i < names.length; i++ ) {
        base = base[ names[i] ] = base[ names[i] ] || {};
    }

    // If a value was given, set it to the last name:
    if( lastName ) base = base[ lastName ] = value;

    // Return the last object in the hierarchy:
    return base;
};

// Usages:

createNestedObject( window, ["shapes", "circle"] );
// Now window.shapes.circle is an empty object, ready to be used.

var obj = {}; // Works with any object other that window too
createNestedObject( obj, ["shapes", "rectangle", "width"], 300 );
// Now we have: obj.shapes.rectangle.width === 300

createNestedObject( obj, "shapes.rectangle.height".split('.'), 400 );
// Now we have: obj.shapes.rectangle.height === 400

Note: if your hierarchy needs to be built from values other that standard objects (ie. not {}), see also TimDog's answer below.

Edit: uses regular loops instead of for...in loops. It's safer in cases where a library modifies the Array prototype.

Solution 3

My ES2015 solution. Keeps existing values.

const set = (obj, path, val) => { 
    const keys = path.split('.');
    const lastKey = keys.pop();
    const lastObj = keys.reduce((obj, key) => 
        obj[key] = obj[key] || {}, 
        obj); 
    lastObj[lastKey] = val;
};

Example:

const obj = {'a': {'prop': {'that': 'exists'}}};
set(obj, 'a.very.deep.prop', 'value');
console.log(JSON.stringify(obj));
// {"a":{"prop":{"that":"exists"},"very":{"deep":{"prop":"value"}}}}

Solution 4

Using ES6 is shorten. Set your path into an array. first, you have to reverse the array, to start filling the object.

let obj = ['a','b','c'] // {a:{b:{c:{}}}
obj.reverse();

const nestedObject = obj.reduce((prev, current) => (
    {[current]:{...prev}}
), {});

Solution 5

Another recursive solution:

var nest = function(obj, keys, v) {
    if (keys.length === 1) {
      obj[keys[0]] = v;
    } else {
      var key = keys.shift();
      obj[key] = nest(typeof obj[key] === 'undefined' ? {} : obj[key], keys, v);
    }

    return obj;
};

Example usage:

var dog = {bark: {sound: 'bark!'}};
nest(dog, ['bark', 'loudness'], 66);
nest(dog, ['woff', 'sound'], 'woff!');
console.log(dog); // {bark: {loudness: 66, sound: "bark!"}, woff: {sound: "woff!"}}
Share:
107,979

Related videos on Youtube

David
Author by

David

Updated on July 05, 2022

Comments

  • David
    David almost 2 years

    I hope someone can help me with this Javascript.

    I have an Object called "Settings" and I would like to write a function that adds new settings to that object.

    The new setting's name and value are provided as strings. The string giving the setting's name is then split by the underscores into an array. The new setting should get added to the existing "Settings" object by creating new nested objects with the names given by each part of the array, except the last part which should be a string giving the setting's value. I should then be able to refer to the setting and e.g. alert its value. I can do this in a static way like this...

    var Settings = {};
    var newSettingName = "Modules_Video_Plugin";
    var newSettingValue = "JWPlayer";
    var newSettingNameArray = newSettingName.split("_");
    
    Settings[newSettingNameArray[0]] = {};
    Settings[newSettingNameArray[0]][newSettingNameArray[1]] = {};
    Settings[newSettingNameArray[0]][newSettingNameArray[1]][newSettingNameArray[2]] = newSettingValue;
    
    alert(Settings.Modules.Mediaplayers.Video.Plugin);
    

    ... the part that creates the nested objects is doing this ...

    Settings["Modules"] = {};
    Settings["Modules"]["Video"] = {};
    Settings["Modules"]["Video"]["Plugin"] = "JWPlayer";
    

    However, as the number of parts that make up the setting name can vary, e.g. a newSettingName could be "Modules_Floorplan_Image_Src", I'd like to do this dynamically using a function such as...

    createSetting (newSettingNameArray, newSettingValue);
    
    function createSetting(setting, value) {
        // code to create new setting goes here
    }
    

    Can anyone help me work out how to do this dynamically?

    I presume there has to be a for...loop in there to itterate through the array, but I haven't been able to work out a way to create the nested objects.

    If you've got this far thanks very much for taking the time to read even if you can't help.

  • Christian
    Christian about 13 years
    Well, I wrote pretty much that same thing, except that I was going to explain newSettingName.split('_'). I don't see the point behind duplicate answers, so there. Maybe explain better your answer though.
  • Bart
    Bart almost 12 years
    I hope you realize that you're answering a question for which the accepted answer is already over 15 months old...
  • Bart
    Bart almost 12 years
    It's okay, it's just not very likely the person who asked the question is still looking for an answer. Your answer is still good (and different from the rest) so I gave you some thumbs up on it.
  • Adrien
    Adrien over 11 years
    Actually, this is the best answer as it behaves as expected.
  • Ken
    Ken almost 11 years
    @jlgrall If I need to call this from another function that handles the actual value assignment, is there a way to return the new key path instead of the key's value (base)?
  • jlgrall
    jlgrall almost 11 years
    @Ken: Well, you already have the key path: its the array of names. As this function returns the last object in the chain, you only need to use the last name on the last object.
  • Ken
    Ken almost 11 years
    @jlgrall Right, it's returning a valid path reference to a different closure that I'm having trouble with. Even if I return the path of the current iteration to the calling closure, when I try an assign a value to it, the value always gets assigned to the last object created. I have a feeling it's because I'm returning a promise, and it's getting resolved to the final object in the final iteration. So I'm looking at a refactor strategy to handle value assignments on the object buildout, like TimDog's answer below.
  • Mrchief
    Mrchief over 9 years
    I've my own recursive version for this, but +1 for avoiding recursion!
  • David Angel
    David Angel about 9 years
    Why on earth does this work and not delete existing nested objects?! You have blown minds, sir.
  • Felipe Alarcon
    Felipe Alarcon over 7 years
    It's never too late to improve an existing answer.
  • Lachezar Raychev
    Lachezar Raychev over 7 years
    what exactly are keyPath and value in that function ?
  • chrisw
    chrisw over 7 years
    I know this is old, but thought I'd chime in. The reason this works is the "base" argument is passed by a copy of the reference to the original object. So on each iteration evaluated from right-to-left base becomes a pointer to the position of the currently assigned property.
  • Manohar Reddy Poreddy
    Manohar Reddy Poreddy over 6 years
    Great one. I did go to last value and hence failed. Understood, searched and found this. Being at a parent level is key and works fine.
  • Manohar Reddy Poreddy
    Manohar Reddy Poreddy over 6 years
    keyPath here for above example, means, the key is Modules.Video.Plugin, and value is JWPlayer, in the json they are trying to build
  • jlgrall
    jlgrall over 6 years
    @HaithamMaya: it works well too :) I edited the examples to show you.
  • temuri
    temuri over 5 years
    Awesome! How can this be changed if nestedObject was defined previously and has {a: {b:{}} set, but not a.b.c. ? I.e. preserving existing keys and only add missing. That would be really useful.
  • tilted
    tilted about 5 years
    @jlgrall Thanks for this.
  • 55 Cancri
    55 Cancri over 4 years
    Perfect. Thank you for this.
  • Blazes
    Blazes over 3 years
    nice slick use of ES6
  • Ajay Jayendran
    Ajay Jayendran over 3 years
    Good one, How do we add an array of objects ? Example { one: [ {} ,{} , {} ], two: [ {},{},{} ] }
  • Regular Jo
    Regular Jo almost 3 years
    It was my honor to give this it's 100th upvote. Haha.
  • Admin
    Admin over 2 years
    Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
  • JC Cavalca
    JC Cavalca over 2 years
    it's perfect and smart thanks.