两个对象。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()相同。

有办法做到这一点吗?


当前回答

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

使用示例:

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

其他回答

我知道这是一个老问题,但在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] } } }

你将在下面的答案中找到一个不可更改的版本。

注意,这将导致循环引用上的无限递归。这里有一些关于如何检测循环引用的很好的答案,如果你认为你会面临这个问题。

当涉及到宿主对象或比值包更复杂的任何类型的对象时,这个问题就不那么简单了

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?

另一件需要记住的事情是:包含循环的对象图。这通常不难处理——简单地保留一组已经访问过的源对象——但经常被遗忘。

您可能应该编写一个深度合并函数,它只期望原始值和简单对象(结构化克隆算法最多可以处理的那些类型)作为合并源。如果遇到它不能处理的东西,或者只是通过引用而不是深度合并进行赋值,则抛出。

换句话说,没有一种适合所有人的算法,您要么必须使用自己的算法,要么寻找恰好涵盖您的用例的库方法。

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

如果你想要一个单行程序,而不需要像lodash那样庞大的库,我建议你使用deepmerge (npm install deepmerge)或deepmerge-ts (npm install deepmerge-ts)。

deepmerge也为TypeScript提供了类型,并且更加稳定(因为它比较老),但是deepmerge-ts也可用于Deno,并且从设计上看更快,尽管顾名思义是用TypeScript编写的。

一旦导入就可以了

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

得到

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

这对于复杂的对象和数组非常有效。这是一个真正的全面解决方案。

我试着写一个对象。基于Object的pollyfill的assignDeep。在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}}}))