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

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

0
我写了一个更简单的深度合并函数,而不使用任何第三方库。

function merge(object1, object2) {
    /* Start iterating over each key of the object. */
    for (const key in object2) {
        /* 1). When object1 has the same key as object2. */
        if (object1[key]) {
            /* 1.1). When both values are type of object then again recursively call merge on those inner objects. */
            if(typeof object1[key] === "object" && typeof object2[key] === "object")
                object1[key] = merge(object1[key], object2[key]);
            /* 1.1). When both values are some other type then update the value in object1 from object2. */
            else
                object1[key] = object2[key];            
        } else {
            /* 2). When object1 doesn't have the same key as object2. */
            if(typeof object2[key] === "object")
                /* 2.1). If the value is of type object, then copy the entire value into object1. */
                Object.assign(object1, { [key]: object2[key] });
            else
                /* 2.2). If both objects are totally different then copy all keys from object2 to object1. */
                Object.assign(object1, object2);
        }
    }
    return object1;
}
const object1 = { a: { a:1 } };
const object2 = { a: { b:1 } };
console.log(merge(object1, object2));

由于我们正在将object2合并到object1中,如果两个对象中具有相同的键和原始值,则会将object2中的键更新到object1中。

0

我尝试编写一个基于mdn上的Object.assign的pollyfill的Object.assignDeep

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


0
有人知道 ES6/ES7 规范中是否存在深层合并(deep merging)吗? Object.assign documentation 指出它不进行深克隆。

0

我已经阅读了所有的答案并组合出了自己的答案。大多数现有的答案不符合我的要求。

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

使用[...new Set(...)]可以去除平面数组中的重复值。

嵌套数组使用concat进行连接。


0

如果您想合并多个普通对象(不修改输入对象),可以基于Object.assign polyfill进行操作。

function isPlainObject(a) {
    return (!!a) && (a.constructor === Object);
}

function merge(target) {
    let to = Object.assign({}, target);

    for (let index = 1; index < arguments.length; index++) {
        let nextSource = arguments[index];

        if (nextSource !== null && nextSource !== undefined) {
            for (let nextKey in nextSource) {
                // Avoid bugs when hasOwnProperty is shadowed
                if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                    if (isPlainObject(to[nextKey]) && isPlainObject(nextSource[nextKey])) {
                        to[nextKey] = merge(to[nextKey], nextSource[nextKey]);
                    } else {
                        to[nextKey] = nextSource[nextKey];
                    }
                }
            }
        }
    }

    return to;
}

// Usage

var obj1 = {
    a: 1,
    b: {
        x: 2,
        y: {
            t: 3,
            u: 4
        }
    },
    c: "hi"
};

var obj2 = {
    b: {
        x: 200,
        y: {
            u: 4000,
            v: 5000
        }
    }
};

var obj3 = {
    c: "hello"
};

console.log("result", merge(obj1, obj2, obj3));
console.log("obj1", obj1);
console.log("obj2", obj2);
console.log("obj3", obj3);

如果您想要进行有限深度的合并

function isPlainObject(a) {
        return (!!a) && (a.constructor === Object);
    }

function merge(target) {
let to = Object.assign({}, target);

const hasDepth = arguments.length > 2 && typeof arguments[arguments.length - 1] === 'number';

const depth = hasDepth ? arguments[arguments.length - 1] : Infinity;

const lastObjectIndex = hasDepth ? arguments.length - 2 : arguments.length - 1;

for (let index = 1; index <= lastObjectIndex; index++) {
    let nextSource = arguments[index];

    if (nextSource !== null && nextSource !== undefined) {
        for (let nextKey in nextSource) {
            // Avoid bugs when hasOwnProperty is shadowed
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                if (depth > 0 && isPlainObject(to[nextKey]) && isPlainObject(nextSource[nextKey])) {
                    to[nextKey] = merge(to[nextKey], nextSource[nextKey], depth - 1);
                } else {
                    to[nextKey] = nextSource[nextKey];
                }
            }
        }
    }
}

return to;
}

// Usage

var obj1 = {
    a: 1,
    b: {
        x: 2,
        y: {
            t: 3,
            u: 4,
            z: {zzz: 100}
        }
    },
    c: "hi"
};

var obj2 = {
    b: {
        y: {
            u: 4000,
            v: 5000,
            z: {}
        }
    }
};

var obj3 = {
    c: "hello"
};

console.log('deep 0', merge(obj1, obj2, obj3, 0));
console.log('deep 1', merge(obj1, obj2, obj3, 1));
console.log('deep 2', merge(obj1, obj2, obj3, 2));
console.log('deep 2', merge(obj1, obj2, obj3, 4));


当两个或多个对象具有数组数据类型时,它不起作用,数组被覆盖。 - dipenparmar12
@dipenparmar12 我们应该仅合并普通对象,因为我们没有明确的规则来合并其他类型。你可以参考 MUI 的 deepmerge:https://github.com/mui-org/material-ui/blob/next/packages/mui-utils/src/deepmerge.ts - Văn Quyết

0

我发现了一种只有两行的解决方案,可以在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' } }


2
这并不是真正的深度合并。它将无法处理 merge({x:{y:{z:1}}}, {x:{y:{w:2}}})。如果 obj2 也有现有值,它也无法更新 obj1 中的现有值,例如 merge({x:{y:1}}, {x:{y:2}}) - Oreille
以上函数稍作修改即可实现深度合并。const deepMerge = (ob1, ob2) => { const temp = Object.assign({}, ob1, ob2); Object.keys(temp).forEach((key) => { if (typeof temp[key] === "object") { temp[key] = deepMerge(ob1[key], ob2[key]); } }); return temp; }; - Sagar Khan
递归调用将不起作用,因为temp将被重新分配,您将无法将结果设置回去。 - saumilsdk
你是否在检查是否超过两层深? - pery mimon

-1
我使用ES6编写了这个深度赋值的方法。
function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item) && item !== null)
}

function deepAssign(...objs) {
    if (objs.length < 2) {
        throw new Error('Need two or more objects to merge')
    }

    const target = objs[0]
    for (let i = 1; i < objs.length; i++) {
        const source = objs[i]
        Object.keys(source).forEach(prop => {
            const value = source[prop]
            if (isObject(value)) {
                if (target.hasOwnProperty(prop) && isObject(target[prop])) {
                    target[prop] = deepAssign(target[prop], value)
                } else {
                    target[prop] = value
                }
            } else if (Array.isArray(value)) {
                if (target.hasOwnProperty(prop) && Array.isArray(target[prop])) {
                    const targetArray = target[prop]
                    value.forEach((sourceItem, itemIndex) => {
                        if (itemIndex < targetArray.length) {
                            const targetItem = targetArray[itemIndex]

                            if (Object.is(targetItem, sourceItem)) {
                                return
                            }

                            if (isObject(targetItem) && isObject(sourceItem)) {
                                targetArray[itemIndex] = deepAssign(targetItem, sourceItem)
                            } else if (Array.isArray(targetItem) && Array.isArray(sourceItem)) {
                                targetArray[itemIndex] = deepAssign(targetItem, sourceItem)
                            } else {
                                targetArray[itemIndex] = sourceItem
                            }
                        } else {
                            targetArray.push(sourceItem)
                        }
                    })
                } else {
                    target[prop] = value
                }
            } else {
                target[prop] = value
            }
        })
    }

    return target
}

-2

虽然它不存在,但你可以使用JSON.parse(JSON.stringify(jobs))


7
这将深拷贝/克隆一个对象,但不会合并两个对象。 - Mike Scotty
这不会将一个对象的方法复制到另一个对象,因为JSON不支持函数类型。 - danwellman
1
如果函数克隆不是问题,你可以这样做...Object.assign({ first: 'object' }, JSON.parse(JSON.stringify({ second: 'object' }))); - FredArters

-2

有一个 lodash 包专门处理对象的深度克隆。优点是您不必包含整个 lodash 库。

它被称为 lodash.clonedeep

在 nodejs 中使用方法如下:

var cloneDeep = require('lodash.clonedeep');
 
const newObject = cloneDeep(oldObject);

在ReactJS中的用法是:
import cloneDeep from 'lodash/cloneDeep';

const newObject = cloneDeep(oldObject);

请查看文档此处。如果您对其工作原理感兴趣,请查看源文件此处


1
问题是关于合并,而不是克隆。 - karl

-4

这很简单而且有效:

let item = {
    firstName: 'Jonnie',
    lastName: 'Walker',
    fullName: function fullName() {
            return 'Jonnie Walker';
    }
Object.assign(Object.create(item), item);

解释:

Object.create()创建新对象。如果您将参数传递给函数,它将为您创建具有其他对象原型的对象。因此,如果对象原型上有任何函数,则它们将传递到其他对象的原型。

Object.assign()合并两个对象并创建全新的对象,它们不再引用原对象。因此,对我来说,这个例子很好用。


不是复制 getter/setter 函数。 - ktretyak
这个“深入”是如何实现的? - dandavis

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