如何深层合并而不是浅层合并?


337

Object.assignObject 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()

有没有办法做到这一点?


深度合并与将属性从一个对象复制到另一个对象相同吗?

2
不可以,因为对象属性不应被覆盖,而是应该将每个子对象合并到目标上的同一子对象(如果已存在)。
Mike

ES6已完成,不再添加新功能,即AFAIK。
kangax


1
@Oriol尽管需要jQuery ...
m0meni 2016年

Answers:


329

有人知道ES6 / ES7规范中是否存在深度合并?

不,不是的。


21
请查看编辑历史记录。在我回答这个问题时,问题是有人知道ES6 / ES7规范中是否存在深度合并?

37
此答案不再适用于此问题-应该更新或删除
DonVaughn

13
问题不应该被编辑到这个程度。编辑是为了澄清。一个新问题应该已经发布。
CJ汤普森

169

我知道这是一个老问题,但是我想出的ES2015 / ES6最简单的解决方案实际上很简单,使用Object.assign(),

希望这会有所帮助:

/**
 * 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);
}

用法示例:

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

您将在下面的答案中找到它的不变版本。

请注意,这将导致循环引用上的无限递归。如果您认为自己会遇到此问题,这里有一些很棒的答案,如何检测循环引用。


1
如果您的对象图包含将导致无限递归的循环
the8472 '16

2
为什么这样写:Object.assign(target, { [key]: {} })如果仅仅是这样target[key] = {}
约尔格·莱尼(JürgLehni),

1
和... target[key] = source[key]而不是Object.assign(target, { [key]: source[key] });
尔格Lehni

3
这不支持中的任何非普通对象target。例如,mergeDeep({a: 3}, {a: {b: 4}})将导致Number物体增强,这显然是不希望的。另外,isObject不接受数组,但接受任何其他本机对象类型,例如Date,不应进行深度复制。
riv

2
据我了解,它不适用于数组吗?
Vedmant

118

您可以使用Lodash合并

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

6
嘿,这是最简单,最漂亮的解决方案。Lodash很棒,他们应该将其包含为核心js对象
Nurbol Alpysbayev,

11
结果不应该{ 'a': [{ 'b': 2 }, { 'c': 3 }, { 'd': 4 }, { 'e': 5 }] }吗?
J. Hesters,

好问题。对于Lodash维护者来说,这可能是一个单独的问题。
AndrewHenderson

7
结果{ 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }是正确的,因为我们正在合并数组的元素。该元件0object.a就是{b: 2},元件0other.a{c: 3}。当这两个因为它们具有相同的数组索引而合并时,结果为{ 'b': 2, 'c': 3 },这是0新对象中的元素。
亚历山德鲁·弗库里塔

我更喜欢这个,它缩小了6倍。
独奏

101

当涉及宿主对象或比一袋值更复杂的任何类型的对象时,问题都不是简单的

  • 调用获取方法以获取值还是复制属性描述符?
  • 如果合并目标具有设置器(自己的属性或其原型链中的),该怎么办?您认为该值已经存在还是调用设置器来更新当前值?
  • 您会调用自有功能还是将其复制过来?如果它们是绑定函数或箭头函数,取决于定义它们时作用域链中的某物怎么办?
  • 如果它像DOM节点该怎么办?您当然不希望将其视为简单对象,而只是将其所有属性深层合并到
  • 如何处理“简单”结构,如数组或映射或集合?认为它们已经存在或合并了吗?
  • 如何处理不可数的自有财产?
  • 那新的子树呢?只是通过引用分配还是进行深克隆?
  • 如何处理冻结/密封/不可扩展的物体?

要记住的另一件事:包含循环的对象图。通常不难处理-只需保留一个Set已经访问过的源对象-但通常会被遗忘。

您可能应该编写一个深度合并函数,该函数仅期望原始值和简单对象(最多是结构化克隆算法可以处理的那些类型)作为合并源。如果遇到任何无法处理的事件或仅通过引用分配而不是深度合并,则抛出该异常。

换句话说,没有一种万能的算法,您要么必须自己滚动,要么寻找碰巧可以涵盖用例的库方法。


2
V8开发人员未实施安全的“文档状态”传输的
借口

您提出了许多好的问题,我很希望看到您的建议的实施。所以我尝试在下面做一个。你能看一下并发表评论吗?stackoverflow.com/a/48579540/8122487
RaphaMex

66

这是@Salakar答案的一个不变的版本(不修改输入内容)。如果您正在执行函数式编程类型的东西,则很有用。

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

1
@torazaburo参见我先前发布的isObject函数
Salakar,2016年

更新了它。经过一些测试之后,我发现了一个深层嵌套对象的错误
CpILL

3
它是一个经过计算的属性名称,第一个将使用的值key作为属性名称,后一个将使“键”成为属性名称。请参阅:es6-features.org/#ComputedPropertyNames
CpILL,2016年

2
isObject你没有需要检查&& item !== null的结束,因为与线开始item &&,不是吗?
星历

2
如果source嵌套的子对象比target更深,则这些对象仍将在mergedDeep的输出中引用相同的值(我认为)。例如, const target = { a: 1 }; const source = { b: { c: 2 } }; const merged = mergeDeep(target, source); merged.b.c; // 2 source.b.c = 3; merged.b.c; // 3 这是个问题吗?它不会改变输入,但是将来对输入的任何突变都可能使输出改变,反之亦然。值得一提的是,ramda R.merge()具有相同的行为。
詹姆斯·康克林

40

由于此问题仍然存在,因此有另一种方法:

  • ES6 / 2015
  • 不可变的(不修改原始对象)
  • 处理数组(将它们连接)

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


很好 但是,当我们具有包含重复元素的数组时,这些元素将串联在一起(存在重复元素)。我对此进行了修改以采用一个参数(数组唯一:true / false)。
宇航员

1
为使阵列独特,您可以更改 prev[key] = pVal.concat(...oVal);prev[key] = [...pVal, ...oVal].filter((element, index, array) => array.indexOf(element) === index);
Richard Herries

1
好干净!绝对是这里最好的答案!
538ROMEO,

辉煌。这也证明了数组被合并,这正是我所寻找的。
Tschallacka

是的,@ CplLL解决方案据说是不可变的,但使用函数内部实际的对象可变性,而使用reduce not则不然。
奥古斯丁·里丁格

30

我知道已经有很多答案,并且有很多评论认为它们行不通。唯一的共识是,它是如此复杂,以至没有人为它制定标准。但是,SO中大多数可接受的答案都暴露了被广泛使用的“简单技巧”。因此,对于像我们这样的人来说,他们不是专家,而是想要通过掌握更多有关javascript的复杂性来编写更安全的代码,我将尝试阐明一些知识。

在弄脏我们的手之前,让我澄清两点:

  • [免责声明]我在下面提出了一个函数,该函数解决了如何深入循环javascript对象中进行复制并说明通常简短地注释过的内容。它尚未投入生产。为了清楚起见,我特意撇开了其他注意事项,例如圆形对象(通过集合或不冲突的符号属性进行跟踪),复制参考值或深克隆,不变的目标对象(是否又是深克隆?),每种对象的类型,都可以通过访问器获取/设置属性...另外,我也没有测试性能-尽管这很重要-因为这也不是重点。
  • 我将使用复制分配术语而不是合并。因为在我看来,合并是保守的,应该在冲突时失败。在这里,发生冲突时,我们希望源覆盖目标。一样Object.assign

与答案for..inObject.keys具有误导性

进行深层复制似乎是一种基本且普遍的做法,因此我们希望找到一个单行,或者至少通过简单的递归来快速获胜。我们不希望我们需要一个库或编写一个100行的自定义函数。

当我第一次阅读Salakar的答案时,我真的以为我可以做得更好,更简单(您可以将其与Object.assignon 进行比较x={a:1}, y={a:{b:1}})。然后我读了8472的答案,然后我想...没有那么容易的事情,改进已经给出的答案不会使我们走的太远。

让我们暂时进行深度复制和递归。只需考虑人们如何(错误地)解析属性以复制一个非常简单的对象。

const y = Object.create(
    { proto : 1 },
    { a: { enumerable: true, value: 1},
      [Symbol('b')] : { enumerable: true, value: 1} } )

Object.assign({},y)
> { 'a': 1, Symbol(b): 1 } // All (enumerable) properties are copied

((x,y) => Object.keys(y).reduce((acc,k) => Object.assign(acc, { [k]: y[k] }), x))({},y)
> { 'a': 1 } // Missing a property!

((x,y) => {for (let k in y) x[k]=y[k];return x})({},y)
> { 'a': 1, 'proto': 1 } // Missing a property! Prototype's property is copied too!

Object.keys将省略自己的不可枚举的属性,自己的符号键属性以及所有原型的属性。如果您的对象没有任何对象,那可能很好。但是请记住,它Object.assign处理自己的符号键可枚举属性。因此,您的自定义副本失去了发展。

for..in将提供源,其原型以及整个原型链的属性,而无需您(或不知道)它。您的目标最终可能具有太多的属性,从而使原型属性和自己的属性混合在一起。

如果你正在写一个通用的功能,你不使用Object.getOwnPropertyDescriptorsObject.getOwnPropertyNamesObject.getOwnPropertySymbolsObject.getPrototypeOf,你很可能就错了。

编写函数之前要考虑的事项

首先,请确保您了解什么是Javascript对象。在Javascript中,对象由其自身的属性和(父)原型对象组成。原型对象又由其自己的属性和原型对象组成。依此类推,定义原型链。

属性是一对键(stringsymbol)和描述符(valueget/ set访问器,以及诸如之类的属性enumerable)。

最后,有许多类型的对象。您可能希望以不同的方式处理对象对象与对象日期或对象功能。

因此,编写深层副本时,至少应回答以下问题:

  1. 我认为深度(适合递归查找)或平坦是什么?
  2. 我要复制哪些属性?(可枚举/不可枚举,字符串键/符号键,自己的属性/原型的自己的属性,值/描述符...)

对于我的示例,我认为只有object Objects是deep,因为其他构造函数创建的其他对象可能不适用于深入的外观。从定制这个SO

function toType(a) {
    // Get fine type (object, array, function, null, error, date ...)
    return ({}).toString.call(a).match(/([a-z]+)(:?\])/i)[1];
}

function isDeepObject(obj) {
    return "Object" === toType(obj);
}

我做了一个options选择复制内容的对象(出于演示目的)。

const options = {nonEnum:true, symbols:true, descriptors: true, proto:true};

拟议职能

您可以在此插件中对其进行测试。

function deepAssign(options) {
    return function deepAssignWithOptions (target, ...sources) {
        sources.forEach( (source) => {

            if (!isDeepObject(source) || !isDeepObject(target))
                return;

            // Copy source's own properties into target's own properties
            function copyProperty(property) {
                const descriptor = Object.getOwnPropertyDescriptor(source, property);
                //default: omit non-enumerable properties
                if (descriptor.enumerable || options.nonEnum) {
                    // Copy in-depth first
                    if (isDeepObject(source[property]) && isDeepObject(target[property]))
                        descriptor.value = deepAssign(options)(target[property], source[property]);
                    //default: omit descriptors
                    if (options.descriptors)
                        Object.defineProperty(target, property, descriptor); // shallow copy descriptor
                    else
                        target[property] = descriptor.value; // shallow copy value only
                }
            }

            // Copy string-keyed properties
            Object.getOwnPropertyNames(source).forEach(copyProperty);

            //default: omit symbol-keyed properties
            if (options.symbols)
                Object.getOwnPropertySymbols(source).forEach(copyProperty);

            //default: omit prototype's own properties
            if (options.proto)
                // Copy souce prototype's own properties into target prototype's own properties
                deepAssign(Object.assign({},options,{proto:false})) (// Prevent deeper copy of the prototype chain
                    Object.getPrototypeOf(target),
                    Object.getPrototypeOf(source)
                );

        });
        return target;
    }
}

可以这样使用:

const x = { a: { a: 1 } },
      y = { a: { b: 1 } };
deepAssign(options)(x,y); // { a: { a: 1, b: 1 } }

13

我用lodash:

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

2
请注意,合并会更改对象,如果您想要不使对象发生变化的东西,则 _cloneDeep(value1).merge(value2)
geckos

3
@geckos您可以进行_.merge({},value1,value2)
Spenhouet

10

这是TypeScript实现:

export const mergeObjects = <T extends object = object>(target: T, ...sources: T[]): T  => {
  if (!sources.length) {
    return target;
  }
  const source = sources.shift();
  if (source === undefined) {
    return target;
  }

  if (isMergebleObject(target) && isMergebleObject(source)) {
    Object.keys(source).forEach(function(key: string) {
      if (isMergebleObject(source[key])) {
        if (!target[key]) {
          target[key] = {};
        }
        mergeObjects(target[key], source[key]);
      } else {
        target[key] = source[key];
      }
    });
  }

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

const isObject = (item: any): boolean => {
  return item !== null && typeof item === 'object';
};

const isMergebleObject = (item): boolean => {
  return isObject(item) && !Array.isArray(item);
};

和单元测试:

describe('merge', () => {
  it('should merge Objects and all nested Ones', () => {
    const obj1 = { a: { a1: 'A1'}, c: 'C', d: {} };
    const obj2 = { a: { a2: 'A2'}, b: { b1: 'B1'}, d: null };
    const obj3 = { a: { a1: 'A1', a2: 'A2'}, b: { b1: 'B1'}, c: 'C', d: null};
    expect(mergeObjects({}, obj1, obj2)).toEqual(obj3);
  });
  it('should behave like Object.assign on the top level', () => {
    const obj1 = { a: { a1: 'A1'}, c: 'C'};
    const obj2 = { a: undefined, b: { b1: 'B1'}};
    expect(mergeObjects({}, obj1, obj2)).toEqual(Object.assign({}, obj1, obj2));
  });
  it('should not merge array values, just override', () => {
    const obj1 = {a: ['A', 'B']};
    const obj2 = {a: ['C'], b: ['D']};
    expect(mergeObjects({}, obj1, obj2)).toEqual({a: ['C'], b: ['D']});
  });
  it('typed merge', () => {
    expect(mergeObjects<TestPosition>(new TestPosition(0, 0), new TestPosition(1, 1)))
      .toEqual(new TestPosition(1, 1));
  });
});

class TestPosition {
  constructor(public x: number = 0, public y: number = 0) {/*empty*/}
}

9

deepmerge npm软件包似乎是解决此问题最广泛使用的库:https ://www.npmjs.com/package/deepmerge


8

我想提出一个非常简单的ES5替代方案。该函数获取2个参数- target并且source必须为“对象”类型。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. 数组+ obj =数组
  2. obj +数组= obj
  3. obj + obj = obj(递归合并)
  4. 数组+数组=数组(concat)

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

看一个例子(如果需要,可以尝试一下)

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

有一个限制-浏览器的调用堆栈长度。现代浏览器会在某些真正深层次的递归上抛出错误(想想成千上万的嵌套调用)。另外,您还可以通过添加新条件和类型检查来随意处理数组+对象等情况。


8

这是另一个ES6解决方案,适用于对象和数组。

function deepMerge(...sources) {
  let acc = {}
  for (const source of sources) {
    if (source instanceof Array) {
      if (!(acc instanceof Array)) {
        acc = []
      }
      acc = [...acc, ...source]
    } else if (source instanceof Object) {
      for (let [key, value] of Object.entries(source)) {
        if (value instanceof Object && key in acc) {
          value = deepMerge(acc[key], value)
        }
        acc = { ...acc, [key]: value }
      }
    }
  }
  return acc
}

3
是经过测试和/或属于库的一部分,看起来不错,但希望确保它已被证明是经过验证的。


7

有没有办法做到这一点?

如果可以将npm库用作解决方案,那么您真正使用的对象合并高级功能将允许您深度合并对象,并使用熟悉的回调函数自定义/覆盖每个合并操作。它的主要思想不仅仅是深度合并-当两个键相同时,值会发生什么?这个库可以解决这个问题-当两个键冲突时,object-merge-advanced权衡类型,目的是在合并后保留尽可能多的数据:

对象键合并权重键值类型以保留尽可能多的数据

第一个自变量的键标记为#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
//      }
//    }

该算法递归地遍历所有输入对象键,进行比较并生成并返回新的合并结果。


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

使用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 '18

5

这里的大多数示例似乎太复杂了,我在自己创建的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

如果您想使用一个内衬而不需要像lodash这样的庞大库,建议您使用deepmerge。(npm install deepmerge

那你就可以

deepmerge({ a: 1, b: 2, c: 3 }, { a: 2, d: 3 });

要得到

{ a: 2, b: 2, c: 3, d: 3 }

令人高兴的是,它随带TypeScript的键入功能。它还允许合并数组。这是一个真正的全方位解决方案。


4

我们可以使用$ .extend(true,object1,object2)进行深度合并。价值真实表示递归地合并两个对象,修改第一。

$ extend(true,target,object)


9
询问者从未表示他们正在使用jquery,并且似乎在要求本机javascript解决方案。
Tee JoE

这是执行此操作的非常简单的方法,并且可以正常工作。如果我是问这个问题的人,我会考虑一个可行的解决方案。:)
kashiraja

这是一个很好的答案,但是缺少指向jQuery源代码的链接。jQuery有很多人在从事该项目,他们花了一些时间使深层复制工作正常。同样,源代码相当“简单”:github.com/jquery/jquery/blob/master/src/core.js#L125 “简单”用引号引起来,因为挖掘时开始变得复杂jQuery.isPlainObject()。这就暴露了确定某物是否为简单物体的复杂性,这在大多数情况下都是很容易错过的。猜猜jQuery用什么语言编写?
CubicleSoft

4

这里简单明了的简单解决方案就像Object.assigndeeep 一样工作,并且无需修改即可用于数组

function deepAssign(target, ...sources) {
    for( source of sources){
        for(let k in source){
            let vs = source[k], vt = target[k];
            if(Object(vs)== vs && Object(vt)===vt ){
                target[k] = deepAssign(vt, vs)
                continue;
            }
            target[k] = source[k];
        }    
    }
    return target;
}

x = { a: { a: 1 }, b:[1,2] };
y = { a: { b: 1 }, b:[3] };
z = {c:3,b:[,,,4]}
x = deepAssign(x,y,z)
// x will be
x ==  {
  "a": {
    "a": 1,
    "b": 1
  },
  "b": [    1,    2,    null,    4  ],
  "c": 3
}


3

加载缓存的Redux状态时遇到此问题。如果我只是加载缓存的状态,则会遇到状态状态已更新的新应用版本的错误。

已经提到过,lodash提供了merge我使用的功能:

const currentInitialState = configureState().getState();
const mergedState = _.merge({}, currentInitialState, cachedState);
const store = configureState(mergedState);

3

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

function merge(current, updates) {
  for (key of Object.keys(updates)) {
    if (!current.hasOwnProperty(key) || typeof updates[key] !== 'object') current[key] = updates[key];
    else merge(current[key], updates[key]);
  }
  return current;
}
console.log(merge({ a: { a: 1 } }, { a: { b: 1 } }));

数组处理:上面的版本用新的覆盖旧的数组值。如果您希望它保留旧的数组值并添加新的值,只需在存储else if (current[key] instanceof Array && updates[key] instanceof Array) current[key] = current[key].concat(updates[key])单元上方添加一个块else即可。


1
我喜欢它,但是它需要对'current'进行简单的未定义检查,否则{foo:undefined}不会合并。只需在for循环之前添加一个if(current)。
Andreas Pardeike

感谢您的建议
Vincent

2

这是我刚写的另一个支持数组的代码。它吸引了他们。

function isObject(obj) {
    return obj !== null && typeof obj === 'object';
}


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(Array.isArray(target)) {
        if(Array.isArray(source)) {
            target.push(...source);
        } else {
            target.push(source);
        }
    } else if(isPlainObject(target)) {
        if(isPlainObject(source)) {
            for(let key of Object.keys(source)) {
                if(!target[key]) {
                    target[key] = source[key];
                } else {
                    mergeDeep(target[key], source[key]);
                }
            }
        } else {
            throw new Error(`Cannot merge object with non-object`);
        }
    } else {
        target = source;
    }

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

2

使用此功能:

merge(target, source, mutable = false) {
        const newObj = typeof target == 'object' ? (mutable ? target : Object.assign({}, target)) : {};
        for (const prop in source) {
            if (target[prop] == null || typeof target[prop] === 'undefined') {
                newObj[prop] = source[prop];
            } else if (Array.isArray(target[prop])) {
                newObj[prop] = source[prop] || target[prop];
            } else if (target[prop] instanceof RegExp) {
                newObj[prop] = source[prop] || target[prop];
            } else {
                newObj[prop] = typeof source[prop] === 'object' ? this.merge(target[prop], source[prop]) : source[prop];
            }
        }
        return newObj;
    }

2

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

2
// copies all properties from source object to dest object recursively
export function recursivelyMoveProperties(source, dest) {
  for (const prop in source) {
    if (!source.hasOwnProperty(prop)) {
      continue;
    }

    if (source[prop] === null) {
      // property is null
      dest[prop] = source[prop];
      continue;
    }

    if (typeof source[prop] === 'object') {
      // if property is object let's dive into in
      if (Array.isArray(source[prop])) {
        dest[prop] = [];
      } else {
        if (!dest.hasOwnProperty(prop)
        || typeof dest[prop] !== 'object'
        || dest[prop] === null || Array.isArray(dest[prop])
        || !Object.keys(dest[prop]).length) {
          dest[prop] = {};
        }
      }
      recursivelyMoveProperties(source[prop], dest[prop]);
      continue;
    }

    // property is simple type: string, number, e.t.c
    dest[prop] = source[prop];
  }
  return dest;
}

单元测试:

describe('recursivelyMoveProperties', () => {
    it('should copy properties correctly', () => {
      const source: any = {
        propS1: 'str1',
        propS2: 'str2',
        propN1: 1,
        propN2: 2,
        propA1: [1, 2, 3],
        propA2: [],
        propB1: true,
        propB2: false,
        propU1: null,
        propU2: null,
        propD1: undefined,
        propD2: undefined,
        propO1: {
          subS1: 'sub11',
          subS2: 'sub12',
          subN1: 11,
          subN2: 12,
          subA1: [11, 12, 13],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
        propO2: {
          subS1: 'sub21',
          subS2: 'sub22',
          subN1: 21,
          subN2: 22,
          subA1: [21, 22, 23],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      };
      let dest: any = {
        propS2: 'str2',
        propS3: 'str3',
        propN2: -2,
        propN3: 3,
        propA2: [2, 2],
        propA3: [3, 2, 1],
        propB2: true,
        propB3: false,
        propU2: 'not null',
        propU3: null,
        propD2: 'defined',
        propD3: undefined,
        propO2: {
          subS2: 'inv22',
          subS3: 'sub23',
          subN2: -22,
          subN3: 23,
          subA2: [5, 5, 5],
          subA3: [31, 32, 33],
          subB2: false,
          subB3: true,
          subU2: 'not null --- ',
          subU3: null,
          subD2: ' not undefined ----',
          subD3: undefined,
        },
        propO3: {
          subS1: 'sub31',
          subS2: 'sub32',
          subN1: 31,
          subN2: 32,
          subA1: [31, 32, 33],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      };
      dest = recursivelyMoveProperties(source, dest);

      expect(dest).toEqual({
        propS1: 'str1',
        propS2: 'str2',
        propS3: 'str3',
        propN1: 1,
        propN2: 2,
        propN3: 3,
        propA1: [1, 2, 3],
        propA2: [],
        propA3: [3, 2, 1],
        propB1: true,
        propB2: false,
        propB3: false,
        propU1: null,
        propU2: null,
        propU3: null,
        propD1: undefined,
        propD2: undefined,
        propD3: undefined,
        propO1: {
          subS1: 'sub11',
          subS2: 'sub12',
          subN1: 11,
          subN2: 12,
          subA1: [11, 12, 13],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
        propO2: {
          subS1: 'sub21',
          subS2: 'sub22',
          subS3: 'sub23',
          subN1: 21,
          subN2: 22,
          subN3: 23,
          subA1: [21, 22, 23],
          subA2: [],
          subA3: [31, 32, 33],
          subB1: false,
          subB2: true,
          subB3: true,
          subU1: null,
          subU2: null,
          subU3: null,
          subD1: undefined,
          subD2: undefined,
          subD3: undefined,
        },
        propO3: {
          subS1: 'sub31',
          subS2: 'sub32',
          subN1: 31,
          subN2: 32,
          subA1: [31, 32, 33],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      });
    });
  });

2

我发现只有2行解决方案可以在javascript中进行深度合并。让我知道这对您有何影响。

const obj1 = { a: { b: "c", x: "y" } }
const obj2 = { a: { b: "d", e: "f" } }
temp = Object.assign({}, obj1, obj2)
Object.keys(temp).forEach(key => {
    temp[key] = (typeof temp[key] === 'object') ? Object.assign(temp[key], obj1[key], obj2[key]) : temp[key])
}
console.log(temp)

临时对象将打印{a:{b:'d',e:'f',x:'y'}}


1
这并没有进行实际的深度合并。它将失败merge({x:{y:{z:1}}}, {x:{y:{w:2}}})。如果obj2也有它们,例如Il,Il也将无法更新obj1中的现有值merge({x:{y:1}}, {x:{y:2}})
Oreilles

1

有时,即使您这样认为,也不需要深度合并。例如,如果您有一个带有嵌套对象的默认配置,并且想用自己的配置深深地扩展它,则可以为此创建一个类。这个概念很简单:

function AjaxConfig(config) {

  // Default values + config

  Object.assign(this, {
    method: 'POST',
    contentType: 'text/plain'
  }, config);

  // Default values in nested objects

  this.headers = Object.assign({}, this.headers, { 
    'X-Requested-With': 'custom'
  });
}

// Define your config

var config = {
  url: 'https://google.com',
  headers: {
    'x-client-data': 'CI22yQEI'
  }
};

// Extend the default values with your own
var fullMergedConfig = new AjaxConfig(config);

// View in DevTools
console.log(fullMergedConfig);

您可以将其转换为函数(而不是构造函数)。


1

这是一种廉价的深度合并,它使用我能想到的最少的代码。每个源都将覆盖先前的属性(如果存在)。

const { keys } = Object;

const isObject = a => typeof a === "object" && !Array.isArray(a);
const merge = (a, b) =>
  isObject(a) && isObject(b)
    ? deepMerge(a, b)
    : isObject(a) && !isObject(b)
    ? a
    : b;

const coalesceByKey = source => (acc, key) =>
  (acc[key] && source[key]
    ? (acc[key] = merge(acc[key], source[key]))
    : (acc[key] = source[key])) && acc;

/**
 * Merge all sources into the target
 * overwriting primitive values in the the accumulated target as we go (if they already exist)
 * @param {*} target
 * @param  {...any} sources
 */
const deepMerge = (target, ...sources) =>
  sources.reduce(
    (acc, source) => keys(source).reduce(coalesceByKey(source), acc),
    target
  );

console.log(deepMerge({ a: 1 }, { a: 2 }));
console.log(deepMerge({ a: 1 }, { a: { b: 2 } }));
console.log(deepMerge({ a: { b: 2 } }, { a: 1 }));

1

我正在使用以下简短函数来深度合并对象。
这对我很有效。
作者完全解释了它的工作原理。

/*!
 * Merge two or more objects together.
 * (c) 2017 Chris Ferdinandi, MIT License, https://gomakethings.com
 * @param   {Boolean}  deep     If true, do a deep (or recursive) merge [optional]
 * @param   {Object}   objects  The objects to merge together
 * @returns {Object}            Merged values of defaults and options
 * 
 * Use the function as follows:
 * let shallowMerge = extend(obj1, obj2);
 * let deepMerge = extend(true, obj1, obj2)
 */

var extend = function () {

    // Variables
    var extended = {};
    var deep = false;
    var i = 0;

    // Check if a deep merge
    if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) {
        deep = arguments[0];
        i++;
    }

    // Merge the object into the extended object
    var merge = function (obj) {
        for (var prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                // If property is an object, merge properties
                if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]') {
                    extended[prop] = extend(extended[prop], obj[prop]);
                } else {
                    extended[prop] = obj[prop];
                }
            }
        }
    };

    // Loop through each object and conduct a merge
    for (; i < arguments.length; i++) {
        merge(arguments[i]);
    }

    return extended;

};

虽然此链接可以回答问题,但最好在此处包括答案的基本部分,并提供链接以供参考。如果链接的页面发生更改,仅链接的答案可能会失效。- 评论
克里斯·卡玛拉塔'19

嗨@ChrisCamaratta。这里不仅是必不可少的部分,还包括所有内容-函数及其使用方法。因此,这绝对不是唯一的答案。这是我用来深度合并对象的功能。仅当您需要作者对其工作原理的解释时,才提供此链接。我认为,比讲授JavaScript的作者更好地解释工作原理对社区是无益的。感谢您的评论。
John Shearing

嗯 我错过了它,或者在查看它时代码没有出现在审阅者界面中。我同意这是一个高质量的答案。看来其他评论家会取代我的初步评估,所以我认为你还好。对不起,灵感旗。
克里斯·卡玛拉塔

大!@ChrisCamaratta,感谢您帮助我了解发生了什么。
John Shearing
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.