How to deep merge instead of shallow merge?

How to deep merge instead of shallow merge?

Both Object.assign and Object spread only do a shallow merge.
An example of the problem:
// No object nesting
const x = { a: 1 }
const y = { b: 1 }
const z = { …x, …y } // { a: 1, b: 1 }

The output is what you’d expect. However if I try this:
// Object nesting
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = { …x, …y } // { a: { b: 1 } }

Instead of
{ a: { a: 1, b: 1 } }

you get
{ a: { b: 1 } }

x is completely overwritten because the spread syntax only goes one level deep. This is the same with Object.assign().
Is there a way to do this?

Solutions/Answers:

Solution 1:

Does anybody know if deep merging exists in the ES6/ES7 spec?

No, it does not.

Solution 2:

I know this is a bit of an old issue but the easiest solution in ES2015/ES6 I could come up with was actually quite simple, using Object.assign(),

Hopefully this helps:

/**
 * Simple object check.
 * @param item
 * @returns {boolean}
 */
export function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item));
}

/**
 * Deep merge two objects.
 * @param target
 * @param ...sources
 */
export function mergeDeep(target, ...sources) {
  if (!sources.length) return target;
  const source = sources.shift();

  if (isObject(target) && isObject(source)) {
    for (const key in source) {
      if (isObject(source[key])) {
        if (!target[key]) Object.assign(target, { [key]: {} });
        mergeDeep(target[key], source[key]);
      } else {
        Object.assign(target, { [key]: source[key] });
      }
    }
  }

  return mergeDeep(target, ...sources);
}

Example usage:

mergeDeep(this, { a: { b: { c: 123 } } });
// or
const merged = mergeDeep({a: 1}, { b : { c: { d: { e: 12345}}}});  
console.dir(merged); // { a: 1, b: { c: { d: [Object] } } }

You’ll find an immutable version of this in the answer below.

Related:  ReactJS: Modeling Bi-Directional Infinite Scrolling

Note that this will lead to infinite recursion on circular references. There’s some great answers on here on how to detect circular references if you think you’d face this issue.

Solution 3:

The problem is non-trivial when it comes to host objects or any kind of object that’s more complex than a bag of values

  • do you invoke a getter to obtain a value or do you copy over the property descriptor?
  • what if the merge target has a setter (either own property or in its prototype chain)? Do you consider the value as already-present or call the setter to update the current value?
  • do you invoke own-property functions or copy them over? What if they’re bound functions or arrow functions depending on something in their scope chain at the time they were defined?
  • what if it’s something like a DOM node? You certainly don’t want to treat it as simple object and just deep-merge all its properties over into
  • how to deal with “simple” structures like arrays or maps or sets? Consider them already-present or merge them too?
  • how to deal with non-enumerable own properties?
  • what about new subtrees? Simply assign by reference or deep clone?
  • how to deal with frozen/sealed/non-extensible objects?
Related:  Read json file content with require vs fs.readFile

Another thing to keep in mind: Object graphs that contain cycles. It’s usually not difficult to deal with – simply keep a Set of already-visited source objects – but often forgotten.

You probably should write a deep-merge function that only expects primitive values and simple objects – at most those types that the structured clone algorithm can handle – as merge sources. Throw if it encounters anything it cannot handle or just assign by reference instead of deep merging.

In other words, there is no one-size-fits-all algorithm, you either have to roll your own or look for a library method that happens to cover your use-cases.

Solution 4:

You can use Lodash merge:

var object = {
  'a': [{ 'b': 2 }, { 'd': 4 }]
};

var other = {
  'a': [{ 'c': 3 }, { 'e': 5 }]
};

_.merge(object, other);
// => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }

Solution 5:

Here is an immutable (does not modify the inputs) version of @Salakar’s answer. Useful if you’re doing functional programming type stuff.

export function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item));
}

export default function mergeDeep(target, source) {
  let output = Object.assign({}, target);
  if (isObject(target) && isObject(source)) {
    Object.keys(source).forEach(key => {
      if (isObject(source[key])) {
        if (!(key in target))
          Object.assign(output, { [key]: source[key] });
        else
          output[key] = mergeDeep(target[key], source[key]);
      } else {
        Object.assign(output, { [key]: source[key] });
      }
    });
  }
  return output;
}

Solution 6:

Since this issue is still active, here’s another approach:

  • ES6/2015
  • Immutable (does not modify original objects)
  • Handles arrays (concatenates them)
/**
* Performs a deep merge of objects and returns new object. Does not modify
* objects (immutable) and merges arrays via concatenation.
*
* @param {...object} objects - Objects to merge
* @returns {object} New object with merged key/values
*/
function mergeDeep(...objects) {
  const isObject = obj => obj && typeof obj === 'object';
  
  return objects.reduce((prev, obj) => {
    Object.keys(obj).forEach(key => {
      const pVal = prev[key];
      const oVal = obj[key];
      
      if (Array.isArray(pVal) && Array.isArray(oVal)) {
        prev[key] = pVal.concat(...oVal);
      }
      else if (isObject(pVal) && isObject(oVal)) {
        prev[key] = mergeDeep(pVal, oVal);
      }
      else {
        prev[key] = oVal;
      }
    });
    
    return prev;
  }, {});
}

// Test objects
const obj1 = {
  a: 1,
  b: 1, 
  c: { x: 1, y: 1 },
  d: [ 1, 1 ]
}
const obj2 = {
  b: 2, 
  c: { y: 2, z: 2 },
  d: [ 2, 2 ],
  e: 2
}
const obj3 = mergeDeep(obj1, obj2);

// Out
console.log(obj3);