两个对象。assign和Object spread只做浅合并。

这个问题的一个例子:

// 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()相同。

有办法做到这一点吗?


当前回答

许多答案使用数十行代码,或者需要向项目添加一个新库,但如果您使用递归,这只是4行代码。

函数合并(当前,更新){ for (Object.keys(updates)的key) { if (!current. hasownproperty (key) || typeof updates[key] !== 'object') current[key] = updates[key]; Else merge(current[key], updates[key]); } 返回当前; } console.log(合并({答:{:1}},{:{b: 1}}));

数组处理:上面的版本用新值覆盖旧的数组值。如果你想保留旧的数组值并添加新的,只需在else语句上方添加一个else If (current[key] instanceof array && updates[key] instanceof array) current[key] = current[key].concat(updates[key])块,你就都设置好了。

其他回答

我把这里所有的答案都看了一遍,然后拼凑出了一个我自己的答案。现有的大多数答案都不是我想要的方式。

这对于2021年来说是相当可怕的,所以任何改善的建议,我都洗耳恭听!

这是在Typescript中

type Props = Record<string, any>

export const deepMerge = (target: Props, ...sources: Props[]): Props => {
  if (!sources.length) {
    return target
  }

  Object.entries(sources.shift() ?? []).forEach(([key, value]) => {
    if (!target[key]) {
      Object.assign(target, { [key]: {} })
    }

    if (
      value.constructor === Object ||
      (value.constructor === Array && value.find(v => v.constructor === Object))
    ) {
      deepMerge(target[key], value)
    } else if (value.constructor === Array) {
      Object.assign(target, {
        [key]: value.find(v => v.constructor === Array)
          ? target[key].concat(value)
          : [...new Set([...target[key], ...value])],
      })
    } else {
      Object.assign(target, { [key]: value })
    }
  })

  return target
}

平面数组使用[…]删除重复值。新的(…)]。

嵌套数组使用concat连接。

下面的函数对对象进行深度复制,它涵盖了复制原语、数组以及对象

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

我想介绍一个相当简单的ES5替代方案。该函数获得2个参数——目标和源,必须为“object”类型。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获取source属性; 如果目标有source属性,而target & source没有 两个对象(4个中的3个),目标的属性被覆盖; 如果target确实有一个source属性,并且它们都是对象/数组(剩余1种情况),那么递归发生合并两个对象(或两个数组的连接);

还要考虑以下几点:

Array + obj = Array Obj + array = Obj Obj + Obj = Obj(递归合并) Array + Array = Array (concat)

它是可预测的,支持基本类型以及数组和对象。我们可以合并两个对象,我认为我们可以通过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));

有一个限制-浏览器的调用堆栈长度。现代浏览器会在一些真正深层的递归中抛出错误(想想成千上万的嵌套调用)。此外,您还可以自由地处理像数组+对象等情况,因为您希望添加新的条件和类型检查。

你可以使用Lodash合并:

Var对象= { 'a': [{'b': 2}, {'d': 4}] }; Var other = { 'a': [{'c': 3}, {'e': 5}] }; console.log(_。合并(对象,其他)); / / = > {a: [{b: 2,“c”:3},{' d ': 4,“e”:5}]} < script src = " https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js " > < /脚本>

我的用例是将默认值合并到配置中。如果我的组件接受一个具有深度嵌套结构的配置对象,并且我的组件定义了默认配置,那么我希望在配置中为未提供的所有配置选项设置默认值。

使用示例:

export default MyComponent = ({config}) => {
  const mergedConfig = mergeDefaults(config, {header:{margins:{left:10, top: 10}}});
  // Component code here
}

这允许我传递一个空配置或空配置,或一个部分配置,并让所有未配置的值回落到它们的默认值。

我的mergeDefaults实现如下所示:

export default function mergeDefaults(config, defaults) {
  if (config === null || config === undefined) return defaults;
  for (var attrname in defaults) {
    if (defaults[attrname].constructor === Object) config[attrname] = mergeDefaults(config[attrname], defaults[attrname]);
    else if (config[attrname] === undefined) config[attrname] = defaults[attrname];
  }
  return config;
}


这些是单元测试

import '@testing-library/jest-dom/extend-expect';
import mergeDefaults from './mergeDefaults';

describe('mergeDefaults', () => {
  it('should create configuration', () => {
    const config = mergeDefaults(null, { a: 10, b: { c: 'default1', d: 'default2' } });
    expect(config.a).toStrictEqual(10);
    expect(config.b.c).toStrictEqual('default1');
    expect(config.b.d).toStrictEqual('default2');
  });
  it('should fill configuration', () => {
    const config = mergeDefaults({}, { a: 10, b: { c: 'default1', d: 'default2' } });
    expect(config.a).toStrictEqual(10);
    expect(config.b.c).toStrictEqual('default1');
    expect(config.b.d).toStrictEqual('default2');
  });
  it('should not overwrite configuration', () => {
    const config = mergeDefaults({ a: 12, b: { c: 'config1', d: 'config2' } }, { a: 10, b: { c: 'default1', d: 'default2' } });
    expect(config.a).toStrictEqual(12);
    expect(config.b.c).toStrictEqual('config1');
    expect(config.b.d).toStrictEqual('config2');
  });
  it('should merge configuration', () => {
    const config = mergeDefaults({ a: 12, b: { d: 'config2' } }, { a: 10, b: { c: 'default1', d: 'default2' }, e: 15 });
    expect(config.a).toStrictEqual(12);
    expect(config.b.c).toStrictEqual('default1');
    expect(config.b.d).toStrictEqual('config2');
    expect(config.e).toStrictEqual(15);
  });
});