How to deep merge instead of shallow merge?

2018-12-31 05:24发布

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?

27条回答
步步皆殇っ
2楼-- · 2018-12-31 05:49

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

查看更多
牵手、夕阳
3楼-- · 2018-12-31 05:52

Is there a way to do this?

If npm libraries can be used as a solution, object-merge-advanced from yours truly allows to merge objects deeply and customise/override every single merge action using a familiar callback function. The main idea of it is more than just deep merging — what happens with the value when two keys are the same? This library takes care of that — when two keys clash, object-merge-advanced weighs the types, aiming to retain as much data as possible after merging:

object key merging weighing key value types to retain as much data as possible

First input argument's key is marked #1, second argument's — #2. Depending on each type, one is chosen for the result key's value. In diagram, "an object" means a plain object (not array etc).

When keys don't clash, they all enter the result.

From your example snippet, if you used object-merge-advanced to merge your code snippet:

const mergeObj = require("object-merge-advanced");
const x = { a: { a: 1 } };
const y = { a: { b: 1 } };
const res = console.log(mergeObj(x, y));
// => res = {
//      a: {
//        a: 1,
//        b: 1
//      }
//    }

It's algorithm recursively traverses all input object keys, compares and builds and returns the new merged result.

查看更多
看风景的人
4楼-- · 2018-12-31 05:53

The following function makes a deep copy of objects, it covers copying primitive, arrays as well as object

 function mergeDeep (target, source)  {
    if (typeof target == "object" && typeof source == "object") {
        for (const key in source) {
            if (source[key] === null && (target[key] === undefined || target[key] === null)) {
                target[key] = null;
            } else if (source[key] instanceof Array) {
                if (!target[key]) target[key] = [];
                //concatenate arrays
                target[key] = target[key].concat(source[key]);
            } else if (typeof source[key] == "object") {
                if (!target[key]) target[key] = {};
                this.mergeDeep(target[key], source[key]);
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}
查看更多
与君花间醉酒
5楼-- · 2018-12-31 05:54

A simple solution with ES5 (overwrite existing value):

function merge(current, update) {
  Object.keys(update).forEach(function(key) {
    // if update[key] exist, and it's not a string or array,
    // we go in one level deeper
    if (current.hasOwnProperty(key) 
        && typeof current[key] === 'object'
        && !(current[key] instanceof Array)) {
      merge(current[key], update[key]);

    // if update[key] doesn't exist in current, or it's a string
    // or array, then assign/overwrite current[key] to update[key]
    } else {
      current[key] = update[key];
    }
  });
  return current;
}

var x = { a: { a: 1 } }
var y = { a: { b: 1 } }

console.log(merge(x, y));

查看更多
姐姐魅力值爆表
6楼-- · 2018-12-31 05:55

I tried to write an Object.assignDeep which is based on the pollyfill of Object.assign on mdn.

(ES5)

Object.assignDeep = function (target, varArgs) { // .length of function is 2
    'use strict';
    if (target == null) { // TypeError if undefined or null
        throw new TypeError('Cannot convert undefined or null to object');
    }

    var to = Object(target);

    for (var index = 1; index < arguments.length; index++) {
        var nextSource = arguments[index];

        if (nextSource != null) { // Skip over if undefined or null
            for (var nextKey in nextSource) {
                // Avoid bugs when hasOwnProperty is shadowed
                if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                    if (typeof to[nextKey] === 'object' 
                        && to[nextKey] 
                        && typeof nextSource[nextKey] === 'object' 
                        && nextSource[nextKey]) {                        
                        Object.assignDeep(to[nextKey], nextSource[nextKey]);
                    } else {
                        to[nextKey] = nextSource[nextKey];
                    }
                }
            }
        }
    }
    return to;
};
console.log(Object.assignDeep({},{a:{b:{c:1,d:1}}},{a:{b:{c:2,e:2}}}))

查看更多
爱死公子算了
7楼-- · 2018-12-31 05:56
function isObject(obj) {
    return obj !== null && typeof obj === 'object';
}
const isArray = Array.isArray;

function isPlainObject(obj) {
    return isObject(obj) && (
        obj.constructor === Object  // obj = {}
        || obj.constructor === undefined // obj = Object.create(null)
    );
}

function mergeDeep(target, ...sources){
    if (!sources.length) return target;
    const source = sources.shift();

    if (isPlainObject(source) || isArray(source)) {
        for (const key in source) {
            if (isPlainObject(source[key]) || isArray(source[key])) {
                if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
                    target[key] = {};
                }else if (isArray(source[key]) && !isArray(target[key])) {
                    target[key] = [];
                }
                mergeDeep(target[key], source[key]);
            } else if (source[key] !== undefined && source[key] !== '') {
                target[key] = source[key];
            }
        }
    }

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

// test...
var source = {b:333};
var source2 = {c:32, arr: [33,11]}
var n = mergeDeep({a:33}, source, source2);
source2.arr[1] = 22;
console.log(n.arr); // out: [33, 11]
查看更多
登录 后发表回答