如何进行深度合并而非浅层合并?

618

Object.assign对象扩展运算符都只进行浅复制。

问题示例:

// No object nesting
const x = { a: 1 }
const y = { b: 1 }
const z = { ...x, ...y } // { a: 1, b: 1 }

输出结果与您期望的相同。但是,如果我尝试这样做:
// Object nesting
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = { ...x, ...y } // { a: { b: 1 } }

与其

{ a: { a: 1, b: 1 } }

你能得到:

{ a: { b: 1 } }

由于展开语法只能深入一层,因此x被完全覆盖。Object.assign()也是如此。
有没有办法解决这个问题?

2
不,因为对象属性不应该被覆盖,相反,如果目标对象已经存在,则每个子对象应该合并到同一个子对象中。 - Mike
1
@Oriol 需要 jQuery... - m0meni
3
const merge = (p, c) => Object.keys(p).forEach(k => !!p[k] && p[k].constructor === Object ? merge(p[k], c[k]) : c[k] = p[k]) - Xaqron
2
你可以查看以下 GitHub 链接,获取简短代码解决方案:https://gist.github.com/ahtcx/0cd94e62691f539160b32ecda18af3d6 - Nwawel A Iroume
显示剩余3条评论
50个回答

15

我使用lodash:

import _ = require('lodash');
value = _.merge(value1, value2);

4
请注意,如果您想要不改变对象的东西,则合并将改变对象,那么 _cloneDeep(value1).merge(value2) - geckos
13
你可以使用 _.merge({}, value1, value2)。 - Spenhouet

12

11

我想提供一个相对简单的ES5替代方案。该函数有两个参数 - targetsource,它们必须是“对象”类型。Target将成为结果对象。 Target保留其所有原始属性,但它们的值可能会被修改。

function deepMerge(target, source) {
if(typeof target !== 'object' || typeof source !== 'object') return false; // target or source or both ain't objects, merging doesn't make sense
for(var prop in source) {
  if(!source.hasOwnProperty(prop)) continue; // take into consideration only object's own properties.
  if(prop in target) { // handling merging of two properties with equal names
    if(typeof target[prop] !== 'object') {
      target[prop] = source[prop];
    } else {
      if(typeof source[prop] !== 'object') {
        target[prop] = source[prop];
      } else {
        if(target[prop].concat && source[prop].concat) { // two arrays get concatenated
          target[prop] = target[prop].concat(source[prop]);
        } else { // two objects get merged recursively
          target[prop] = deepMerge(target[prop], source[prop]); 
        } 
      }  
    }
  } else { // new properties get added to target
    target[prop] = source[prop]; 
  }
}
return target;
}

案例:

  • 如果target没有source属性,则将其添加到target中;
  • 如果target具有source属性且targetsource不都是对象(4种情况中的3种),则target的属性将被覆盖;
  • 如果target具有source属性并且它们都是对象/数组(剩下的1种情况),则会发生递归,合并两个对象(或两个数组的连接);

还需考虑以下内容:

  1. 数组 + 对象 = 数组
  2. 对象 + 数组 = 对象
  3. 对象 + 对象 = 对象(递归合并)
  4. 数组 + 数组 = 数组(连接)

它是可预测的,支持原始类型以及数组和对象。由于我们可以合并2个对象,因此我认为我们可以通过reduce函数合并多个对象。

看一个示例(如果您愿意,请随意尝试)

var a = {
   "a_prop": 1,
   "arr_prop": [4, 5, 6],
   "obj": {
     "a_prop": {
       "t_prop": 'test'
     },
     "b_prop": 2
   }
};

var b = {
   "a_prop": 5,
   "arr_prop": [7, 8, 9],
   "b_prop": 15,
   "obj": {
     "a_prop": {
       "u_prop": false
     },
     "b_prop": {
        "s_prop": null
     }
   }
};

function deepMerge(target, source) {
    if(typeof target !== 'object' || typeof source !== 'object') return false;
    for(var prop in source) {
    if(!source.hasOwnProperty(prop)) continue;
      if(prop in target) {
        if(typeof target[prop] !== 'object') {
          target[prop] = source[prop];
        } else {
          if(typeof source[prop] !== 'object') {
            target[prop] = source[prop];
          } else {
            if(target[prop].concat && source[prop].concat) {
              target[prop] = target[prop].concat(source[prop]);
            } else {
              target[prop] = deepMerge(target[prop], source[prop]); 
            } 
          }  
        }
      } else {
        target[prop] = source[prop]; 
      }
    }
  return target;
}

console.log(deepMerge(a, b));

浏览器的调用栈长度存在限制。当递归层数非常深(数千个嵌套调用)时,现代浏览器会抛出错误。此外,您可以通过添加新条件和类型检查来自由处理类似数组+对象等情况。


这对我有用!谢谢。在我的代码中加入了你的贡献!!:-D - Seth Eden

8
有没有一种方法可以做到这一点?如果可以使用npm库作为解决方案,那么我自己的object-merge-advanced允许深度合并对象并使用熟悉的回调函数自定义/覆盖每个单个合并操作。它的主要思想不仅仅是深度合并 - 当两个键相同时,该库会处理值的情况是什么?当两个键冲突时,object-merge-advanced会权衡类型,以尽可能保留合并后的数据:

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

第一个输入参数的键标记为#1,第二个参数的键标记为#2。根据每种类型,选择一个结果键的值。在图表中,“一个对象”表示一个普通对象(不是数组等)。

当键不冲突时,它们都进入结果。

从您的示例片段中,如果您使用object-merge-advanced来合并代码片段:

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

它的算法递归遍历所有输入对象键,进行比较、构建并返回新合并的结果。

这张信息图表中日期和函数在哪里? - vsync

7

使用ES5的简单解决方案(覆盖现有值):

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


正是我所需要的 - ES6 在构建中出现了问题 - 这个 ES5 的替代方案非常棒。 - danday74

6
以下函数可以深度复制对象,它可以复制基本类型、数组和对象。
 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;
}

6

这里的大部分例子都过于复杂,我使用了自己创建的TypeScript例子,我认为它应该能涵盖大多数情况(我将数组视为常规数据,仅替换它们)。

const isObject = (item: any) => typeof item === 'object' && !Array.isArray(item);

export const merge = <A = Object, B = Object>(target: A, source: B): A & B => {
  const isDeep = (prop: string) =>
    isObject(source[prop]) && target.hasOwnProperty(prop) && isObject(target[prop]);
  const replaced = Object.getOwnPropertyNames(source)
    .map(prop => ({ [prop]: isDeep(prop) ? merge(target[prop], source[prop]) : source[prop] }))
    .reduce((a, b) => ({ ...a, ...b }), {});

  return {
    ...(target as Object),
    ...(replaced as Object)
  } as A & B;
};

以下是纯JS的代码,以防万一:

const isObject = item => typeof item === 'object' && !Array.isArray(item);

const merge = (target, source) => {
  const isDeep = prop => 
    isObject(source[prop]) && target.hasOwnProperty(prop) && isObject(target[prop]);
  const replaced = Object.getOwnPropertyNames(source)
    .map(prop => ({ [prop]: isDeep(prop) ? merge(target[prop], source[prop]) : source[prop] }))
    .reduce((a, b) => ({ ...a, ...b }), {});

  return {
    ...target,
    ...replaced
  };
};

这里是我的测试案例,展示了如何使用它。
describe('merge', () => {
  context('shallow merges', () => {
    it('merges objects', () => {
      const a = { a: 'discard' };
      const b = { a: 'test' };
      expect(merge(a, b)).to.deep.equal({ a: 'test' });
    });
    it('extends objects', () => {
      const a = { a: 'test' };
      const b = { b: 'test' };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: 'test' });
    });
    it('extends a property with an object', () => {
      const a = { a: 'test' };
      const b = { b: { c: 'test' } };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: { c: 'test' } });
    });
    it('replaces a property with an object', () => {
      const a = { b: 'whatever', a: 'test' };
      const b = { b: { c: 'test' } };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: { c: 'test' } });
    });
  });

  context('deep merges', () => {
    it('merges objects', () => {
      const a = { test: { a: 'discard', b: 'test' }  };
      const b = { test: { a: 'test' } } ;
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: 'test' } });
    });
    it('extends objects', () => {
      const a = { test: { a: 'test' } };
      const b = { test: { b: 'test' } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: 'test' } });
    });
    it('extends a property with an object', () => {
      const a = { test: { a: 'test' } };
      const b = { test: { b: { c: 'test' } } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: { c: 'test' } } });
    });
    it('replaces a property with an object', () => {
      const a = { test: { b: 'whatever', a: 'test' } };
      const b = { test: { b: { c: 'test' } } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: { c: 'test' } } });
    });
  });
});

如果您认为我缺少某些功能,请告诉我。


5

使用reduce函数

export const merge = (objFrom, objTo) => Object.keys(objFrom)
    .reduce(
        (merged, key) => {
            merged[key] = objFrom[key] instanceof Object && !Array.isArray(objFrom[key])
                ? merge(objFrom[key], merged[key] ?? {})
                : objFrom[key]
            return merged
        }, { ...objTo }
    )

test('merge', async () => {
    const obj1 = { par1: -1, par2: { par2_1: -21, par2_5: -25 }, arr: [0,1,2] }
    const obj2 = { par1: 1, par2: { par2_1: 21 }, par3: 3, arr: [3,4,5] }
    const obj3 = merge3(obj1, obj2)
    expect(obj3).toEqual(
        { par1: -1, par2: { par2_1: -21, par2_5: -25 }, par3: 3, arr: [0,1,2] }
    )
})

5
如果您正在使用ImmutableJS,则可以使用mergeDeep
fromJS(options).mergeDeep(options2).toJS();

4
Ramda是一个很好的JavaScript函数库,具有mergeDeepLeft和mergeDeepRight。这两个函数都可用于此问题。请查看此处的文档:https://ramdajs.com/docs/#mergeDeepLeft
对于特定的示例,我们可以使用以下代码:
import { mergeDeepLeft } from 'ramda'
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = mergeDeepLeft(x, y)) // {"a":{"a":1,"b":1}}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接