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

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个回答

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

14
提问者从未表明他们正在使用jQuery,似乎是在寻求一种本地JavaScript解决方案。 - Teh JoE
这是一种非常简单的方法,它能够正常工作。如果我是提问者,我会考虑这个可行的解决方案。 :) - kashiraja
这是一个非常好的答案,但缺少指向jQuery源代码的链接。jQuery有很多人在项目上工作,并花费了一些时间使深度复制正常工作。此外,源代码相当“简单”:https://github.com/jquery/jquery/blob/master/src/core.js#L125 “简单”用引号括起来,因为当深入挖掘jQuery.isPlainObject()时,它开始变得复杂。这暴露了确定某个东西是否为纯对象的复杂性,大多数答案都没有涉及到。猜猜jQuery是用什么语言编写的? - CubicleSoft

4

我不喜欢任何现有的解决方案。所以,我继续写了自己的。

Object.prototype.merge = function(object) {
    for (const key in object) {
        if (object.hasOwnProperty(key)) {
            if (typeof this[key] == 'object' && typeof object[key] == 'object') {
                this[key].merge(object[key]);
                continue;
            }

            this[key] = object[key];
        }
    }

    return this;
}

它将会像这样使用:

const object = {
    health: 100,
    position: {
        x: 0,
        y: 10
    }
};

object.merge({
    health: 99,
    position: {
        x: 10
    },
    extension: null
});

这将导致:

{
    health: 99,
    position: {
        x: 10,
        y: 10
    }
}

我希望这能帮助那些难以理解正在发生的事情的人。我在这里看到了很多无意义的变量被使用。

谢谢


这将仅合并存在于this中的属性,也许应该使用object.hasOwnProperty(key)而不是this.hasOwnProperty(key) - Giuliano Collacchioni
@GiulianoCollacchioni 很好的发现!我在制作这个时真的很累,没有用我的大脑思考。 - Calculamatrise
它仍然无法复制函数。 - Vishal Kumar Sahu
@VishalKumarSahu 这就是重点!函数在任何情况下都不需要被复制。但如果你有理由这么做,你可以用原始对象的原型中的一个检查函数替换检查它是否为对象的if语句。像这样:(typeof this[key] == 'object' && typeof object[key] == 'object') || (typeof object[key] == 'function' && !(key in Reflect.getPrototypeOf(this))) - Calculamatrise

3

我在加载缓存的redux状态时遇到了问题。如果我只是加载缓存的状态,那么在具有更新状态结构的新应用程序版本中就会遇到错误。

已经提到过,lodash提供了merge函数,我使用了它:

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

3

新方法 | 更新答案

从 node v17 开始,有 structuredClone,根据参考资料:

使用结构化克隆算法创建给定值的深度克隆。

因此,我们可以像这样使用它来合并 2 个对象:

const deepMerge = (obj1, obj2) => {
  const clone1 = structuredClone(obj1);
  const clone2 = structuredClone(obj2);

  for (let key in clone2) {
    if (clone2[key] instanceof Object && clone1[key] instanceof Object) {
      clone1[key] = deepMerge(clone1[key], clone2[key]);
    } else {
      clone1[key] = clone2[key];
    }
  }

  return clone1;
};


const first = { a: { x: 'x', y: 'y' }, b: 1 };
const second = { a: { x: 'xx' }, c: 2 };

const result = deepMerge(first, second);

console.log(result); // { a: { x: 'xx', y: 'y' }, b: 1, c: 2 }


2

这是一个简单的深度合并方法,代码量尽可能少。每个源在存在属性时覆盖前一个属性。

原始答案:Original Answer

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

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

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

示例用法:

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

这使我能够传递一个空的或null的配置,或者部分配置,并且所有未配置的值都会回退到它们的默认值。
我的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);
  });
});


2

另一种使用递归的变体,希望你觉得它有用。

const merge = (obj1, obj2) => {

    const recursiveMerge = (obj, entries) => {
         for (const [key, value] of entries) {
            if (typeof value === "object") {
               obj[key] = obj[key] ? {...obj[key]} : {};
               recursiveMerge(obj[key], Object.entries(value))
            else {
               obj[key] = value;
            }
          }

          return obj;
    }

    return recursiveMerge(obj1, Object.entries(obj2))
}

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

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