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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/27936772/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 00:57:59  来源:igfitidea点击:

How to deep merge instead of shallow merge?

javascriptspread-syntax

提问by Mike

Both Object.assignand Object spreadonly do a shallow merge.

无论Object.assign对象传播只能做一浅合并。

An example of the problem:

问题的一个例子:

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

The output is what you'd expect. However if I try this:

输出是您所期望的。但是,如果我尝试这样做:

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

Instead of

代替

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

you get

你得到

{ a: { b: 1 } }

x is completely overwritten because the spread syntax only goes one level deep. This is the same with Object.assign().

x 被完全覆盖,因为扩展语法只深入一层。这与Object.assign().

Is there a way to do this?

有没有办法做到这一点?

采纳答案by Mike

Does anybody know if deep merging exists in the ES6/ES7 spec?

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

No, it does not.

不,不是的。

回答by Salakar

I know this is a bit of an old issue but the easiest solution in ES2015/ES6 I could come up with was actually quite simple, using Object.assign(),

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

Hopefully this helps:

希望这有助于:

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

Example usage:

用法示例:

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

You'll find an immutable version of this in the answer below.

你会在下面的答案中找到一个不可变的版本。

Note that this will lead to infinite recursion on circular references.There's some great answers on here on how to detect circular references if you think you'd face this issue.

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

回答by AndrewHenderson

You can use Lodash merge:

您可以使用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 }] }

回答by the8472

The problem is non-trivial when it comes to host objects or any kind of object that's more complex than a bag of values

当涉及到宿主对象或任何比一袋值更复杂的对象时,这个问题很重要

  • 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?
  • 您是调用 getter 来获取值还是复制属性描述符?
  • 如果合并目标有一个 setter(要么是自己的属性要么在它的原型链中)怎么办?你认为这个值已经存在还是调用 setter 来更新当前值?
  • 你是调用自有属性函数还是复制它们?如果它们是绑定函数或箭头函数,取决于它们被定义时作用域链中的某些东西怎么办?
  • 如果它类似于 DOM 节点呢?你当然不想把它当作简单的对象,只是将它的所有属性深度合并到
  • 如何处理像数组、映射或集合这样的“简单”结构?考虑它们已经存在还是也合并它们?
  • 如何处理不可枚举的自身属性?
  • 新的子树呢?简单地通过引用分配还是深度克隆?
  • 如何处理冻结/密封/不可扩展的对象?

Another thing to keep in mind: Object graphs that contain cycles. It's usually not difficult to deal with - simply keep a Setof already-visited source objects - but often forgotten.

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

You probably should write a deep-merge function that only expects primitive values and simple objects - at most those types that the structured clone algorithm can handle- as merge sources. Throw if it encounters anything it cannot handle or just assign by reference instead of deep merging.

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

In other words, there is no one-size-fits-all algorithm, you either have to roll your own or look for a library method that happens to cover your use-cases.

换句话说,没有一刀切的算法,您要么必须推出自己的算法,要么寻找恰好涵盖您的用例的库方法。

回答by CpILL

Here is an immutable (does not modify the inputs) version of @Salakar's answer. Useful if you're doing functional programming type stuff.

这是@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;
}

回答by jhildenbiddle

Since this issue is still active, here's another approach:

由于这个问题仍然存在,这里有另一种方法:

  • ES6/2015
  • Immutable (does not modify original objects)
  • Handles arrays (concatenates them)
  • 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);

回答by RaphaMex

I know there's a lot of answers already and as many comments arguing they won't work. The only consensus is that it's so complicated that nobody made a standard for it. However, most of accepted answers in SO expose "simple tricks" that are widely used. So, for all of us like me who are no experts but want to write safer code by grasping a little more about javascript's complexity, I'll try to shed some light.

我知道已经有很多答案,并且有很多评论认为它们不起作用。唯一的共识是它太复杂了,没有人为它制定标准。但是,SO 中大多数公认的答案都公开了广泛使用的“简单技巧”。因此,对于像我这样不是专家但希望通过更多地了解 javascript 的复杂性来编写更安全的代码的所有人,我将尝试阐明一些观点。

Before getting our hands dirty, let me clarify 2 points:

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

  • [DISCLAIMER] I propose a function below that tackles how we deep loopinto javascript objectsfor copy and illustrates what is generally too shortly commented. It is not production-ready. For sake of clarity, I have purposedly left aside other considerations like circular objects (track by a set or unconflicting symbol property), copying reference value or deep clone, immutable destination object (deep clone again?), case-by-case study of each type of objects, get/set properties via accessors... Also, I did not test performance -although it's important- because it's not the point here either.
  • I'll use copyor assignterms instead of merge. Because in my mind a mergeis conservative and should fail upon conflicts. Here, when conflicting, we want the source to overwrite the destination. Like Object.assigndoes.
  • [免责声明] 我在下面提出了一个函数,它解决了我们如何深入循环javascript 对象进行复制,并说明了通常评论太短的内容。它不是生产就绪的。为清楚起见,我特意将其他考虑因素放在一边,例如圆形对象(通过设置或不冲突的符号属性跟踪)、复制参考值或深度克隆、不可变目标对象(再次进行深度克隆?)、逐案研究每种类型的对象,通过访问器获取/设置属性......此外,我没有测试性能 - 尽管它很重要 - 因为它也不是这里的重点。
  • 我将使用copyassign术语而不是merge。因为在我看来,合并是保守的,应该在发生冲突时失败。在这里,当发生冲突时,我们希望源覆盖目标。喜欢Object.assign

Answers with for..inor Object.keysare misleading

带有for..inObject.keys误导性的回答

Making a deep copy seems so basic and common practice that we expect to find a one-liner or, at least, a quick win via simple recursion. We don't expect we should need a library or write a custom function of 100 lines.

制作深层副本似乎是非常基本和普遍的做法,以至于我们希望找到一个单行或至少通过简单的递归快速获胜。我们不希望我们需要一个库或编写一个 100 行的自定义函数。

When I first read Salakar's answer, I genuinely thought I could do better and simpler (you can compare it with Object.assignon x={a:1}, y={a:{b:1}}). Then I read the8472's answerand I thought... there is no getting away so easily, improving already given answers won't get us far.

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

Let's let deep copy and recursive aside an instant. Just consider how (wrongly) people parse properties to copy a very simple object.

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

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.keyswill omit own non-enumerable properties, own symbol-keyed properties and all prototype's properties. It may be fine if your objects don't have any of those. But keep it mind that Object.assignhandles own symbol-keyed enumerable properties. So your custom copy lost its bloom.

Object.keys将省略自己的不可枚举属性、自己的符号键属性和所有原型的属性。如果您的对象没有任何这些,那可能没问题。但请记住,它Object.assign处理自己的符号键可枚举属性。所以你的自定义副本失去了它的绽放。

for..inwill provide properties of the source, of its prototype and of the full prototype chain without you wanting it (or knowing it). Your target may end up with too many properties, mixing up prototype properties and own properties.

for..in将在您不想要(或不知道)的情况下提供源、其原型和完整原型链的属性。你的目标最终可能有太多的属性,混淆了原型属性和自己的属性。

If you're writing a general purpose function and you're not using Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames, Object.getOwnPropertySymbolsor Object.getPrototypeOf, you're most probably doing it wrong.

如果您正在编写通用函数并且没有使用Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames,Object.getOwnPropertySymbolsObject.getPrototypeOf,那么您很可能做错了。

Things to consider before writing your function

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

First, make sure you understand what a Javascript object is. In Javascript, an object is made of its own properties and a (parent) prototype object. The prototype object in turn is made of its own properties and a prototype object. And so on, defining a prototype chain.

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

A property is a pair of key (stringor symbol) and descriptor (valueor get/setaccessor, and attributes like enumerable).

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

Finally, there are many types of objects. You may want to handle differently an object Object from an object Date or an object Function.

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

So, writing your deep copy, you should answer at least those questions:

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

  1. What do I consider deep (proper for recursive look up) or flat?
  2. What properties do I want to copy? (enumerable/non-enumerable, string-keyed/symbol-keyed, own properties/prototype's own properties, values/descriptors...)
  1. 我认为什么是深的(适合递归查找)或平坦的?
  2. 我要复制哪些属性?(可枚举/不可枚举、字符串键/符号键、自己的属性/原型自己的属性、值/描述符...)

For my example, I consider that only the object Objects are deep, because other objects created by other constructors may not be proper for an in-depth look. Customized from this SO.

对于我的示例,我认为只有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);
}

And I made an optionsobject to choose what to copy (for demo purpose).

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

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

Proposed function

建议的功能

You can test it in this plunker.

您可以在这个 plunker 中测试它。

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

That can be used like this:

可以这样使用:

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

回答by Jeff Tian

I use lodash:

我使用 lodash:

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

回答by am0wa

Here is TypeScript implementation:

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

And Unit Tests:

和单元测试:

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

回答by user3336882

The deepmerge npm package appears to be the most widely used library for solving this problem: https://www.npmjs.com/package/deepmerge

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