3

I have underscore.

I have:

var person = {
  personal: {
    fname: 'Victor',
    lname: 'Lee',
    address: {
      street: '1234 Main',
      state: {
        abbrName: 'CA',
        fullName: 'California',
        timezone: 'PST'
      },
      zip: '94043'
    }
  }
};

I would like to update multiple properties but leave others untouched.

Instead of writing three seperate lines:

  person.personal.address.state.abbrName = 'OR';
  person.personal.address.state.fullName = 'Oregon';
  person.personal.address.zip = '97062';

I want to be able to set all props in one line of code, but also leave other properties untouched.

If I do:

_.extend(person.personal.address, {
  state: {
    abbrName: 'OR',
    fullName: 'Oregon'
  },
  zip: '97032'
});

The resulting object has the time zone hacked out:

{
  personal: {
    fname: 'Victor',
    lname: 'Lee',
    address: {
      street: '1234 Main',
      state: {
        abbrName: 'CA',
        fullName: 'California',
      },
      zip: '94043'
    }
  }
};

Something like this would be ideal:

var updateObj = function(obj, key, value){
  // stuff
  return obj;
};

and run like:

updateObj(person, 'personal.address', {
  state: {
    abbrName: 'OR',
    fullName: 'Oregon'
  },
  zip: '97032'
});

So far I've got this but it only completely overwrites one property at a time.

var updateObjectWithStringKey = function(obj, key, value) {
  if (typeof key === "string"){
    key = key.split(".");
  };

  if (prop.length > 1) {
    var e = key.shift();
    updateObjectWithStringKey(obj[e] =
      typeof obj[e] == 'object' ? obj[e] : {},
      key,
      value);
  } else {
    obj[key[0]] = value;
  };

  return obj;

};

EDIT

Ok, I think I'm getting close:

var MergeRecursive = function(destination, source) {

  for (var p in source) {

    if ( typeof source[p] == 'object' ) {
      destination[p] = MergeRecursive(destination[p], source[p]);
    } else {
      destination[p] = source[p];
    };

  };

  return destination;

};

This merges information even if its wedged between levels in an object:

var person = {
  personal: {
    fname: 'Victor',
    lname: 'Lee',
    address: {
      street: '1234 Main',
      state: {
        abbrName: 'CA',
        fullName: 'California',
        timezone: 'PST'
      },
      zip: '94043'
    }
  }
};

var updatedInfo = {
  personal: {
    address: {
      state: {
        abbrName: 'OR',
        fullName: 'Oregon',
        capital: 'Salem'
      },
      zip: '97062'
    },
  }
};

MergeRecursive(person, updatedInfo);

returns

{
  personal: {
    fname: 'Victor',
    lname: 'Lee',
    address: {
      street: '1234 Main',
      state: {
        abbrName: 'OR',
        fullName: 'Oregon',
        timezone: 'PST',
        capital: 'Salem'
      },
      zip: '97062'
    }
  }
}

But like I said, I would like to provide a string path to the part of the object that I would like to update:

updateObj(person, 'personal.address', {
  state: {
    abbrName: 'OR',
    fullName: 'Oregon',
    capital: 'Salem'
  },
  zip: '97062'
});

This function does it but not with the merging behavior above:

var updateObjectWithStringProp = function(obj, prop, value) {

  if (typeof prop === "string") {
    var prop = prop.split('.');
  }

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

  return obj;

};

How do I edit this function to also include the merging behavior?

fuzzybabybunny
  • 5,146
  • 6
  • 32
  • 58
  • 1
    In the particular case outlined above, is there a reason why you didn't also try `_.extend(person.personal.address, { state: 'MO', zip: 44444 })` ? – Ken Franqueiro Sep 28 '15 at 03:15
  • 1
    Any reason why you're using `Object.prototype.toString.call(obj[e]) === "[object Object]"` rather than `typeof obj[e] == 'object'`? – RobG Sep 28 '15 at 03:27
  • Use the mergerecursive function from [this](http://stackoverflow.com/a/383245/502613) question. It'll work. – Jorg Sep 28 '15 at 03:29
  • Ok, I've edited the original question. The big issue is updating info that is wedged between two levels of the object. – fuzzybabybunny Sep 28 '15 at 03:32
  • @Jorg—that's a pretty ordinary answer. *try..catch* is completely unnecessary and `obj2[p].constructor == Object` will fail in many cases. – RobG Sep 28 '15 at 03:33
  • @RobG Agreed it could be tidied up. However, it [does work](http://jsfiddle.net/7kxzfwjt/1/). At least, in the latest Chrome... – Jorg Sep 28 '15 at 03:40
  • @Jorg—maybe, for some meaning of "works". There are many pages of attempts [*here*](http://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-clone-an-object/122704#122704), please ignore the accepted answer. – RobG Sep 28 '15 at 03:51
  • Jorg's answer works in my case. Is there a more robust way to do it? I personally also have a hard time wrapping my head around recursion. – fuzzybabybunny Sep 28 '15 at 03:55
  • Ok, I've updated the OP again with my attempt. Still would like an option to just provide a string path to the part of the object that I would like to update. – fuzzybabybunny Sep 28 '15 at 04:36

2 Answers2

0

You can use recursion, but remember, it can have issues with big objects with high nesting.

Note: You should consider few things before making such function

  • Both values can be simple object
  • Both values can be array of object
  • Both values can be array
  • New object can have extra parameters. Should you allow adding values to original object?
  • Can source object be blank? If yes, should you return undefined or newValues?

There can be other parameters as well.

function mergeObject(source, newValue, allowNewProps) {
  if (typeof newValue === "object" && typeof source === "object") {
    for (var k in newValue) {
      if (Array.isArray(newValue[k]) && Array.isArray(source[k])) {
        for (var i = 0; i < newValue[k].length; i++) {
          mergeObject(source[k][i], newValue[k][i]);
        }
      }
      if (typeof newValue[k] === "object") {
        mergeObject(source[k], newValue[k]);
      } else if (source[k] !== undefined || allowNewProps) {
        source[k] = newValue[k];
      }
    }
  }
}
Rajesh
  • 24,354
  • 5
  • 48
  • 79
-1

Is it possible to change your library from underscore to lodash?

Then you will have the same (and a lot more) functions available, including _.defaultsDeep.

You can perform the merge in 1 single line, preserving your timezone in the middle.

var person = {
  personal: {
    fname: 'Victor',
    lname: 'Lee',
    address: {
      street: '1234 Main',
      state: {
        abbrName: 'CA',
        fullName: 'California',
        timezone: 'PST'
      },
      zip: '94043'
    }
  }
};
var update = {
  personal: { 
    address: { 
      state: { 
        abbrName: 'OR', 
        fullName: 'Oregon' 
      }, 
      zip: '97032'
    }
  }
}
var result = _.defaultsDeep(update, person);

The result would resolve to:

result = {
  "personal":{
    "address":{
      "state":{
        "abbrName":"OR",
        "fullName":"Oregon",
        "timezone":"PST"
      },
      "zip":"97032",
      "street":"1234 Main"
    },
    "fname":"Victor",
    "lname":"Lee"
  }
}