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

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

1
function isObject(obj) {
    return obj !== null && typeof obj === 'object';
}
const isArray = Array.isArray;

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 (isPlainObject(source) || isArray(source)) {
        for (const key in source) {
            if (isPlainObject(source[key]) || isArray(source[key])) {
                if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
                    target[key] = {};
                }else if (isArray(source[key]) && !isArray(target[key])) {
                    target[key] = [];
                }
                mergeDeep(target[key], source[key]);
            } else if (source[key] !== undefined && source[key] !== '') {
                target[key] = source[key];
            }
        }
    }

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

// test...
var source = {b:333};
var source2 = {c:32, arr: [33,11]}
var n = mergeDeep({a:33}, source, source2);
source2.arr[1] = 22;
console.log(n.arr); // out: [33, 11]

return mergeDeep(target, ...sources); 会导致一些递归吗?或者我有什么遗漏吗?或者它是否修改了原始值? - Vishal Kumar Sahu

1

适用于对象和数组的原始脚本解决方案:

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

function deepmerge() {
  merge = function () {
    let target = arguments[0];
    for (let i = 1; i < arguments.length ; i++) {
      let arr = arguments[i];
            for (let k in arr) {
         if (Array.isArray(arr[k])) {
            if (target[k] === undefined) {            
                 target[k] = [];
            }            
            target[k] = [...new Set(target[k].concat(...arr[k]))];
         } else if (typeof arr[k] === 'object') {
            if (target[k] === undefined) {            
                 target[k] = {};
            }
            target[k] = merge(target[k], arr[k]);
         } else {
              target[k] = arr[k];         
         }
      }
    }
    return target;
  }
  return merge(...arguments);
}
console.log(deepmerge(x,y));

输出:

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

1

有一些维护良好的库已经可以做到这一点。npm注册表上的一个例子是merge-deep


1

简单递归解决方案

使用Object.entries,对其中一个对象进行迭代。如果条目不存在,则添加该条目,并在该条目为对象时进行递归。

const x = { a: { a: 1 } }
const y = { a: { b: 1 } }

const z = JSON.parse(JSON.stringify(y))

const mergeIntoZ = (firstObj, secondObj) => {
  Object.entries(firstObj)
    .forEach(([key, value]) => {
      if (secondObj[key] === undefined) {
        secondObj[key] = value
      } else if (typeof value === 'object') {
        mergeIntoZ(firstObj[key], secondObj[key])
      }
    })

}
mergeIntoZ(x, z)
console.log(z)


1

简单、无依赖、不可变(返回新对象)的deepMerge

不会在非对象字段上进行智能处理,b[key]会覆盖a[key]

每个键只访问一次。

使用structuredClone

function deepMerge(a, b) {
  const result = {};
  for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
    result[key] =
      a[key]?.constructor === Object && b[key]?.constructor === Object
        ? deepMerge(a[key], b[key])
        : structuredClone(b[key] !== undefined ? b[key] : a[key]);
  }
  return result;
}


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;

};

虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。- 来自审查 - Chris Camaratta
嗨@ChrisCamaratta。这里不仅有必要的部分,而且全部都在这里——函数以及如何使用它。因此,这绝对不是一个仅包含链接的答案。这是我一直在使用的深度合并对象的函数。如果您想了解作者如何工作的解释,那么链接只是一个参考。我认为,试图比教授JavaScript的作者更好地解释工作原理将对社区构成不公正的行为。感谢您的评论。 - John Shearing
哦,可能是我错过了,或者当我审查时代码没有出现在审阅界面中。我同意这是一个高质量的答案。看起来其他审阅者覆盖了我的初步评估,所以我认为你没问题了。对于 Inspiration 标记我感到抱歉。 - Chris Camaratta
太好了!@ChrisCamaratta,感谢您帮助我理解发生了什么。 - John Shearing

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 defaultConf = {
    prop1: 'config1',
    prop2: 'config2'
}

我们可以通过执行以下操作定义更具体的配置:

const moreSpecificConf = {
    ...defaultConf,
    prop3: 'config3'
}

但是,如果这些配置包含嵌套结构,那么这种方法就不再适用了。
因此,我编写了一个函数,只合并对象中的 { key: value, ... } 部分,并替换其他部分。
const isObject = (val) => val === Object(val);

const merge = (...objects) =>
    objects.reduce(
        (obj1, obj2) => ({
            ...obj1,
            ...obj2,
            ...Object.keys(obj2)
                .filter((key) => key in obj1 && isObject(obj1[key]) && isObject(obj2[key]))
                .map((key) => ({[key]: merge(obj1[key], obj2[key])}))
                .reduce((n1, n2) => ({...n1, ...n2}), {})
        }),
        {}
    );

0

(本地解决方案)如果您知道要深度合并的属性,则

const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
Object.assign(y.a, x.a);
Object.assign(x, y);
// output: a: {b: 1, a: 1}

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