¿Cómo fusionar profundamente en lugar de fusionar superficialmente?


337

Tanto Object.assign como Object spread solo hacen una fusión superficial.

Un ejemplo del problema:

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

El resultado es lo que esperarías. Sin embargo, si intento esto:

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

En vez de

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

usted obtiene

{ a: { b: 1 } }

x se sobrescribe por completo porque la sintaxis de propagación solo tiene un nivel de profundidad. Esto es lo mismo con Object.assign().

¿Hay alguna forma de hacer esto?


¿Es la fusión profunda lo mismo que copiar propiedades de un objeto a otro?

2
No, dado que las propiedades de los objetos no deben sobrescribirse, cada objeto secundario debe fusionarse en el mismo elemento secundario en el destino si ya existe.
Mike

ES6 está finalizado y ya no se agregan nuevas funciones, AFAIK.
kangax


1
@Oriol requiere jQuery aunque ...
m0meni

Respuestas:


330

¿Alguien sabe si existe una fusión profunda en la especificación ES6 / ES7?

No, no lo hace.


21
Por favor revise el historial de edición. Cuando respondí esto, la pregunta era ¿Alguien sabe si existe una fusión profunda en la especificación ES6 / ES7? .

37
Esta respuesta ya no se aplica a esta pregunta: debe actualizarse o eliminarse
DonVaughn

13
La pregunta no debería haber sido editada en este grado. Las ediciones son para aclarar. Se debería haber publicado una nueva pregunta.
CJ Thompson

170

Sé que este es un problema un poco antiguo, pero la solución más fácil en ES2015 / ES6 que pude encontrar fue en realidad bastante simple, usando Object.assign (),

Esperemos que esto ayude:

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

Ejemplo de uso:

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

Encontrarás una versión inmutable de esto en la respuesta a continuación.

Tenga en cuenta que esto conducirá a una recursión infinita en referencias circulares. Aquí hay algunas respuestas excelentes sobre cómo detectar referencias circulares si crees que enfrentarías este problema.


1
si su gráfico de objeto contiene ciclos que conducirán a una recursión infinita
8472 del

2
¿Por qué escribir esto: Object.assign(target, { [key]: {} })si pudiera ser simplemente target[key] = {}?
Jürg Lehni

1
... y en target[key] = source[key]lugar deObject.assign(target, { [key]: source[key] });
Jürg Lehni

3
Esto no admite ningún objeto no plano en target. Por ejemplo, mergeDeep({a: 3}, {a: {b: 4}})dará como resultado un Numberobjeto aumentado , que claramente no se desea. Además, isObjectno acepta matrices, pero acepta cualquier otro tipo de objeto nativo, como Date, que no debe copiarse en profundidad.
riv

2
¿No funciona con matrices como lo entiendo?
Vedmant

119

Puede usar la fusión de 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 }] }

66
Hola gente, esta es la solución más simple y hermosa. Lodash es increíble, deberían incluirlo como objeto core js
Nurbol Alpysbayev

11
¿No debería ser el resultado { 'a': [{ 'b': 2 }, { 'c': 3 }, { 'd': 4 }, { 'e': 5 }] }?
J. Hesters

Buena pregunta. Esa podría ser una pregunta separada o una para los mantenedores de Lodash.
AndrewHenderson

77
El resultado { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }es correcto, porque estamos fusionando elementos de una matriz. El elemento 0de object.aes {b: 2}, el elemento 0de other.aes {c: 3}. Cuando estos dos se fusionan porque tienen el mismo índice de matriz, el resultado es { 'b': 2, 'c': 3 }, que es el elemento 0en el nuevo objeto.
Alexandru Furculita

Prefiero este , es 6 veces más pequeño comprimido.
Solo

101

El problema no es trivial cuando se trata de objetos host o cualquier tipo de objeto que sea más complejo que una bolsa de valores

  • ¿Invoca a un captador para obtener un valor o copia el descriptor de propiedad?
  • ¿Qué pasa si el objetivo de fusión tiene un establecedor (propiedad propia o en su cadena de prototipo)? ¿Considera que el valor ya está presente o llama al configurador para actualizar el valor actual?
  • ¿Invoca funciones de propiedad propia o las copia? ¿Qué pasa si son funciones ligadas o funciones de flecha dependiendo de algo en su cadena de alcance en el momento en que se definieron?
  • ¿Qué pasa si es algo así como un nodo DOM? Ciertamente no desea tratarlo como un objeto simple y simplemente fusionar todas sus propiedades en
  • ¿Cómo lidiar con estructuras "simples" como matrices o mapas o conjuntos? ¿Considerarlos ya presentes o fusionarlos también?
  • ¿Cómo lidiar con propiedades propias no enumerables?
  • ¿Qué pasa con los nuevos subárboles? ¿Simplemente asignar por referencia o clon profundo?
  • ¿Cómo lidiar con objetos congelados / sellados / no extensibles?

Otra cosa a tener en cuenta: gráficos de objetos que contienen ciclos. Por lo general, no es difícil de tratar, simplemente mantenga un Setobjeto fuente ya visitado, pero a menudo se olvida.

Probablemente debería escribir una función de fusión profunda que solo espere valores primitivos y objetos simples, en la mayoría de los tipos que el algoritmo de clonación estructurada puede manejar , como fuentes de fusión. Lanza si encuentra algo que no puede manejar o simplemente asignar por referencia en lugar de una fusión profunda.

En otras palabras, no existe un algoritmo único para todos, ya sea que tenga que usar el suyo propio o buscar un método de biblioteca que cubra sus casos de uso.


2
excusas para que los desarrolladores de V8 no implementen una transferencia segura de "estado de documento"
neaumusic

Planteas muchos problemas y me hubiera encantado ver una implementación de tu recomendación. Así que intenté hacer uno a continuación. ¿Podrías echar un vistazo y comentar? stackoverflow.com/a/48579540/8122487
RaphaMex

66

Aquí hay una versión inmutable (no modifica las entradas) de la respuesta de @ Salakar. Útil si estás haciendo cosas de tipo de programación funcional.

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

1
@torazaburo vea la publicación anterior de mí para la función
isObject

actualizado después de algunas pruebas encontré un error con los objetos profundamente anidados
CpILL

3
Es un nombre de propiedad calculado, el primero usará el valor de keycomo el nombre de la propiedad, el segundo hará que "clave" sea el nombre de la propiedad. Ver: es6-features.org/#ComputedPropertyNames
CpILL

2
en isObjectque no es necesario para comprobar && item !== nullal final, ya que la línea comienza con item &&, ¿no?
Ephemer

2
Si la fuente ha anidado objetos secundarios más profundos que el destino, esos objetos seguirán haciendo referencia a los mismos valores en mergedDeepla salida (creo). Por ejemplo, const target = { a: 1 }; const source = { b: { c: 2 } }; const merged = mergeDeep(target, source); merged.b.c; // 2 source.b.c = 3; merged.b.c; // 3 ¿es esto un problema? No muta las entradas, pero cualquier futura mutación a las entradas podría mutar la salida, y viceversa con mutaciones a las entradas de mutación de salida. Sin embargo, por lo que vale, ramda's R.merge()tiene el mismo comportamiento.
James Conkling

40

Dado que este problema aún está activo, aquí hay otro enfoque:

  • ES6 / 2015
  • Inmutable (no modifica objetos originales)
  • Maneja matrices (las concatena)

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


Esto es bonito. Sin embargo, cuando tenemos una matriz con elementos repetidos, estos se concatenan (hay elementos repetidos). Lo adapté para tomar un parámetro (matrices únicas: verdadero / falso).
Astronauta

1
Para hacer que los arreglos sean únicos, puede cambiar prev[key] = pVal.concat(...oVal);aprev[key] = [...pVal, ...oVal].filter((element, index, array) => array.indexOf(element) === index);
Richard Herries

1
Muy agradable y limpio! Definitivamente la mejor respuesta aquí!
538ROMEO

Glorioso. Este también demuestra que las matrices se fusionan, que es lo que estaba buscando.
Tschallacka

Sí, se dice que la solución @CplLL es inmutable, pero usa mutabilidad de objeto real dentro de la función mientras reduce que no lo hace.
Augustin Riedinger

30

Sé que ya hay muchas respuestas y tantos comentarios argumentando que no funcionarán. El único consenso es que es tan complicado que nadie hizo un estándar para ello. . Sin embargo, la mayoría de las respuestas aceptadas en SO exponen "trucos simples" que son ampliamente utilizados. Por lo tanto, para todos nosotros, como yo, que no somos expertos pero queremos escribir código más seguro al comprender un poco más sobre la complejidad de JavaScript, intentaré arrojar algo de luz.

Antes de ensuciarnos las manos, déjenme aclarar 2 puntos:

  • [Aviso Legal] propongo a continuación una función que aborda la forma en que lo profundo de bucle en objetos de JavaScript para copiar e ilustra lo que generalmente se comenta demasiado brevemente. No está listo para la producción. En aras de la claridad, he dejado de lado deliberadamente otras consideraciones como objetos circulares (seguimiento por un conjunto o propiedad de símbolo no conflictiva) , copia del valor de referencia o clon profundo , objeto de destino inmutable (¿clon profundo nuevamente?), Estudio caso por caso de cada tipo de objetos , obtener / establecer propiedades a través de los accesores ... Además, no probé el rendimiento, aunque es importante, porque tampoco es el punto aquí.
  • Voy a usar copia o asignar términos en lugar de fusionar . Porque en mi opinión, una fusión es conservadora y debería fallar en los conflictos. Aquí, cuando entra en conflicto, queremos que la fuente sobrescriba el destino. Al igual que lo Object.assignhace.

Respuestas con for..ino Object.keysson engañosas

Hacer una copia profunda parece una práctica tan básica y común que esperamos encontrar una línea o, al menos, una victoria rápida a través de una recursión simple. No esperamos que necesitemos una biblioteca o escribir una función personalizada de 100 líneas.

Cuando leí por primera vez vez la respuesta de Salakar , realmente pensé que podría hacerlo mejor y más simple (puedes compararlo con Object.assignon x={a:1}, y={a:{b:1}}). Luego leí la respuesta del 8472 y pensé ... no hay escapatoria tan fácil, mejorar las respuestas ya dadas no nos llevará lejos.

Dejemos una copia profunda y recursiva a un lado al instante. Solo considere cómo (erróneamente) las personas analizan las propiedades para copiar un objeto muy simple.

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.keysomitirá sus propias propiedades no enumerables, sus propias propiedades con clave de símbolo y todas las propiedades del prototipo. Puede estar bien si tus objetos no tienen ninguno de esos. Pero tenga en cuenta que Object.assignmaneja sus propias propiedades enumerables con clave de símbolo. Entonces su copia personalizada perdió su floración.

for..inproporcionará propiedades de la fuente, de su prototipo y de la cadena completa del prototipo sin que lo desee (o lo sepa). Su objetivo puede terminar con demasiadas propiedades, mezclando propiedades prototipo y propiedades propias.

Si estás escribiendo una función de propósito general y no se está usando Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames, Object.getOwnPropertySymbolsoObject.getPrototypeOf , lo más probable que estés haciendo mal.

Cosas a considerar antes de escribir su función

Primero, asegúrese de comprender qué es un objeto Javascript. En Javascript, un objeto está hecho de sus propias propiedades y un objeto prototipo (padre). El objeto prototipo a su vez está hecho de sus propias propiedades y un objeto prototipo. Y así sucesivamente, definiendo una cadena prototipo.

Una propiedad es un par de clave ( stringo symbol) y descriptor ( valueo get/ setaccesor, y atributos comoenumerable ).

Finalmente, hay muchos tipos de objetos . Es posible que desee manejar de manera diferente un objeto Objeto de un objeto Fecha o una función de objeto.

Entonces, al escribir su copia profunda, debe responder al menos esas preguntas:

  1. ¿Qué considero profundo (adecuado para la búsqueda recursiva) o plano?
  2. ¿Qué propiedades quiero copiar? (enumerable / no enumerable, con clave de cadena / con clave de símbolo, propiedades propias / propiedades propias del prototipo, valores / descriptores ...)

Para mi ejemplo, considero que solo los object Objects son profundos , porque otros objetos creados por otros constructores pueden no ser adecuados para una mirada en profundidad. Personalizado a partir de este 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);
}

E hice un optionsobjeto para elegir qué copiar (para fines de demostración).

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

Función propuesta

Puedes probarlo en este 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;
    }
}

Eso se puede usar así:

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

13

Yo uso lodash:

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

2
Tenga en cuenta que la fusión alterará el objeto, si desea algo que no mute el objeto, entonces _cloneDeep(value1).merge(value2)
geckos

3
@geckos Puedes hacer _.merge ({}, value1, value2)
Spenhouet

10

Aquí está la implementación de 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);
};

Y pruebas unitarias:

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


8

Me gustaría presentar una alternativa ES5 bastante simple. La función recibe 2 parámetros - targety sourceque debe ser del tipo "objeto". Targetserá el objeto resultante. Targetconserva todas sus propiedades originales, pero sus valores pueden modificarse.

function deepMerge(target, source) {
if(typeof target !== 'object' || typeof source !== 'object') return false; // target or source or both ain't objects, merging doesn't make sense
for(var prop in source) {
  if(!source.hasOwnProperty(prop)) continue; // take into consideration only object's own properties.
  if(prop in target) { // handling merging of two properties with equal names
    if(typeof target[prop] !== 'object') {
      target[prop] = source[prop];
    } else {
      if(typeof source[prop] !== 'object') {
        target[prop] = source[prop];
      } else {
        if(target[prop].concat && source[prop].concat) { // two arrays get concatenated
          target[prop] = target[prop].concat(source[prop]);
        } else { // two objects get merged recursively
          target[prop] = deepMerge(target[prop], source[prop]); 
        } 
      }  
    }
  } else { // new properties get added to target
    target[prop] = source[prop]; 
  }
}
return target;
}

casos:

  • si targetno tiene una sourcepropiedad, la targetobtiene;
  • si targettiene una sourcepropiedad y target& sourceno son ambos objetos (3 casos de 4), targetla propiedad de se anula;
  • si targettiene una sourcepropiedad y ambos son objetos / matrices (1 caso restante), entonces la recursión ocurre fusionando dos objetos (o concatenación de dos matrices);

También considere lo siguiente :

  1. array + obj = array
  2. obj + array = obj
  3. obj + obj = obj (fusión recursiva)
  4. matriz + matriz = matriz (concat)

Es predecible, admite tipos primitivos, así como matrices y objetos. Además, como podemos fusionar 2 objetos, creo que podemos fusionar más de 2 mediante la función de reducción .

Eche un vistazo a un ejemplo (y juegue con él si lo desea) :

var a = {
   "a_prop": 1,
   "arr_prop": [4, 5, 6],
   "obj": {
     "a_prop": {
       "t_prop": 'test'
     },
     "b_prop": 2
   }
};

var b = {
   "a_prop": 5,
   "arr_prop": [7, 8, 9],
   "b_prop": 15,
   "obj": {
     "a_prop": {
       "u_prop": false
     },
     "b_prop": {
        "s_prop": null
     }
   }
};

function deepMerge(target, source) {
    if(typeof target !== 'object' || typeof source !== 'object') return false;
    for(var prop in source) {
    if(!source.hasOwnProperty(prop)) continue;
      if(prop in target) {
        if(typeof target[prop] !== 'object') {
          target[prop] = source[prop];
        } else {
          if(typeof source[prop] !== 'object') {
            target[prop] = source[prop];
          } else {
            if(target[prop].concat && source[prop].concat) {
              target[prop] = target[prop].concat(source[prop]);
            } else {
              target[prop] = deepMerge(target[prop], source[prop]); 
            } 
          }  
        }
      } else {
        target[prop] = source[prop]; 
      }
    }
  return target;
}

console.log(deepMerge(a, b));

Hay una limitación: la longitud de la pila de llamadas del navegador. Los navegadores modernos arrojarán un error en un nivel de recursión realmente profundo (piense en miles de llamadas anidadas). También es libre de tratar situaciones como matriz + objeto, etc., como desee agregando nuevas condiciones y comprobaciones de tipo.


8

Aquí hay otra solución ES6, funciona con objetos y matrices.

function deepMerge(...sources) {
  let acc = {}
  for (const source of sources) {
    if (source instanceof Array) {
      if (!(acc instanceof Array)) {
        acc = []
      }
      acc = [...acc, ...source]
    } else if (source instanceof Object) {
      for (let [key, value] of Object.entries(source)) {
        if (value instanceof Object && key in acc) {
          value = deepMerge(acc[key], value)
        }
        acc = { ...acc, [key]: value }
      }
    }
  }
  return acc
}

3
está probado y / o es parte de una biblioteca, se ve bien pero me encantaría asegurarme de que esté algo probado.


7

¿Hay alguna forma de hacer esto?

Si las bibliotecas npm se pueden usar como una solución, object-merge-advanced de la suya realmente permite fusionar objetos profundamente y personalizar / anular cada acción de fusión utilizando una función de devolución de llamada familiar. La idea principal es más que una fusión profunda: ¿qué sucede con el valor cuando dos claves son iguales ? Esta biblioteca se encarga de eso: cuando dos claves chocan, object-merge-advancedpesa los tipos, con el objetivo de retener la mayor cantidad de datos posible después de la fusión:

clave de objeto que combina tipos de valores de clave de pesaje para retener la mayor cantidad de datos posible

La clave del primer argumento de entrada está marcada # 1, el segundo argumento - # 2. Dependiendo de cada tipo, se elige uno para el valor de la clave de resultado. En el diagrama, "un objeto" significa un objeto simple (no una matriz, etc.).

Cuando las teclas no chocan, todas ingresan el resultado.

Desde su fragmento de ejemplo, si solía object-merge-advancedfusionar su fragmento de código:

const mergeObj = require("object-merge-advanced");
const x = { a: { a: 1 } };
const y = { a: { b: 1 } };
const res = console.log(mergeObj(x, y));
// => res = {
//      a: {
//        a: 1,
//        b: 1
//      }
//    }

Su algoritmo atraviesa recursivamente todas las claves de objetos de entrada, compara y construye y devuelve el nuevo resultado combinado.


6

La siguiente función realiza una copia profunda de los objetos, abarca la copia de primitivas, matrices y objetos.

 function mergeDeep (target, source)  {
    if (typeof target == "object" && typeof source == "object") {
        for (const key in source) {
            if (source[key] === null && (target[key] === undefined || target[key] === null)) {
                target[key] = null;
            } else if (source[key] instanceof Array) {
                if (!target[key]) target[key] = [];
                //concatenate arrays
                target[key] = target[key].concat(source[key]);
            } else if (typeof source[key] == "object") {
                if (!target[key]) target[key] = {};
                this.mergeDeep(target[key], source[key]);
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

6

Una solución simple con ES5 (sobrescribir el valor existente):

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


justo lo que necesitaba - es6 estaba causando problemas en la construcción - esta alternativa es5 es la bomba
danday74

5

La mayoría de los ejemplos aquí parecen demasiado complejos, estoy usando uno en TypeScript que creé, creo que debería cubrir la mayoría de los casos (estoy manejando matrices como datos regulares, solo reemplazándolos).

const isObject = (item: any) => typeof item === 'object' && !Array.isArray(item);

export const merge = <A = Object, B = Object>(target: A, source: B): A & B => {
  const isDeep = (prop: string) =>
    isObject(source[prop]) && target.hasOwnProperty(prop) && isObject(target[prop]);
  const replaced = Object.getOwnPropertyNames(source)
    .map(prop => ({ [prop]: isDeep(prop) ? merge(target[prop], source[prop]) : source[prop] }))
    .reduce((a, b) => ({ ...a, ...b }), {});

  return {
    ...(target as Object),
    ...(replaced as Object)
  } as A & B;
};

Lo mismo en JS simple, por si acaso:

const isObject = item => typeof item === 'object' && !Array.isArray(item);

const merge = (target, source) => {
  const isDeep = prop => 
    isObject(source[prop]) && target.hasOwnProperty(prop) && isObject(target[prop]);
  const replaced = Object.getOwnPropertyNames(source)
    .map(prop => ({ [prop]: isDeep(prop) ? merge(target[prop], source[prop]) : source[prop] }))
    .reduce((a, b) => ({ ...a, ...b }), {});

  return {
    ...target,
    ...replaced
  };
};

Aquí están mis casos de prueba para mostrar cómo podría usarlo

describe('merge', () => {
  context('shallow merges', () => {
    it('merges objects', () => {
      const a = { a: 'discard' };
      const b = { a: 'test' };
      expect(merge(a, b)).to.deep.equal({ a: 'test' });
    });
    it('extends objects', () => {
      const a = { a: 'test' };
      const b = { b: 'test' };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: 'test' });
    });
    it('extends a property with an object', () => {
      const a = { a: 'test' };
      const b = { b: { c: 'test' } };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: { c: 'test' } });
    });
    it('replaces a property with an object', () => {
      const a = { b: 'whatever', a: 'test' };
      const b = { b: { c: 'test' } };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: { c: 'test' } });
    });
  });

  context('deep merges', () => {
    it('merges objects', () => {
      const a = { test: { a: 'discard', b: 'test' }  };
      const b = { test: { a: 'test' } } ;
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: 'test' } });
    });
    it('extends objects', () => {
      const a = { test: { a: 'test' } };
      const b = { test: { b: 'test' } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: 'test' } });
    });
    it('extends a property with an object', () => {
      const a = { test: { a: 'test' } };
      const b = { test: { b: { c: 'test' } } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: { c: 'test' } } });
    });
    it('replaces a property with an object', () => {
      const a = { test: { b: 'whatever', a: 'test' } };
      const b = { test: { b: { c: 'test' } } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: { c: 'test' } } });
    });
  });
});

Avíseme si cree que me falta alguna funcionalidad.


5

Si desea tener una línea única sin requerir una gran biblioteca como lodash, le sugiero que use deepmerge . ( npm install deepmerge)

Entonces puedes hacer

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

Llegar

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

Lo bueno es que viene con tipings para TypeScript de inmediato. También permite fusionar matrices . Una verdadera solución integral es esta.


4

Podemos usar $ .extend (true, object1, object2) para una fusión profunda. Valor verdadero denota fusionar dos objetos de forma recursiva, modificando el primero.

$ extender (verdadero, objetivo, objeto)


99
El autor de la pregunta nunca indicó que están usando jquery y parece estar pidiendo una solución javascript nativa.
Teh JoE

Esta es una forma muy simple de hacer esto y funciona. Una solución viable que consideraría si fuera yo quien hiciera esta pregunta. :)
kashiraja

Esta es una muy buena respuesta, pero le falta un enlace al código fuente de jQuery. jQuery tiene muchas personas trabajando en el proyecto y han pasado algún tiempo haciendo copias profundas funcionando correctamente. Además, el código fuente es bastante "simple": github.com/jquery/jquery/blob/master/src/core.js#L125 "Simple" está entre comillas porque comienza a complicarse al profundizar jQuery.isPlainObject(). Eso expone la complejidad de determinar si algo es o no un objeto simple, que la mayoría de las respuestas aquí pasan por alto. ¿Adivina en qué idioma está escrito jQuery?
CubicleSoft

4

Aquí una solución simple y directa que funciona como Object.assignsimplemente profundizar y trabajar para una matriz, sin ninguna modificación

function deepAssign(target, ...sources) {
    for( source of sources){
        for(let k in source){
            let vs = source[k], vt = target[k];
            if(Object(vs)== vs && Object(vt)===vt ){
                target[k] = deepAssign(vt, vs)
                continue;
            }
            target[k] = source[k];
        }    
    }
    return target;
}

Ejemplo

x = { a: { a: 1 }, b:[1,2] };
y = { a: { b: 1 }, b:[3] };
z = {c:3,b:[,,,4]}
x = deepAssign(x,y,z)
// x will be
x ==  {
  "a": {
    "a": 1,
    "b": 1
  },
  "b": [    1,    2,    null,    4  ],
  "c": 3
}


3

Estaba teniendo este problema al cargar un estado redux en caché. Si solo cargara el estado en caché, me encontraría con errores para la nueva versión de la aplicación con una estructura de estado actualizada.

Ya se mencionó que lodash ofrece la mergefunción que usé:

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

3

Muchas respuestas usan decenas de líneas de código, o requieren agregar una nueva biblioteca al proyecto, pero si usa la recursión, esto es solo 4 líneas de código.

function merge(current, updates) {
  for (key of Object.keys(updates)) {
    if (!current.hasOwnProperty(key) || typeof updates[key] !== 'object') current[key] = updates[key];
    else merge(current[key], updates[key]);
  }
  return current;
}
console.log(merge({ a: { a: 1 } }, { a: { b: 1 } }));

Manejo de matrices: la versión anterior sobrescribe los valores de matrices antiguos con otros nuevos. Si desea mantener los valores de la matriz anterior y agregar los nuevos, simplemente agregue un else if (current[key] instanceof Array && updates[key] instanceof Array) current[key] = current[key].concat(updates[key])bloque sobre el estado elsey ya está todo listo.


1
Me gusta, pero necesita una simple verificación indefinida para 'actual' o de lo contrario {foo: undefined} no se fusiona. Simplemente agregue un if (actual) antes del ciclo for.
Andreas Pardeike

Gracias por la sugerencia
Vincent

2

Aquí hay otro que acabo de escribir que admite matrices. Los concatena.

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

2

Utiliza esta función:

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

Ramda, que es una buena biblioteca de funciones de JavaScript, tiene mergeDeepLeft y mergeDeepRight. Cualquiera de estos funciona bastante bien para este problema. Consulte la documentación aquí: https://ramdajs.com/docs/#mergeDeepLeft

Para el ejemplo específico en cuestión podemos usar:

import { mergeDeepLeft } from 'ramda'
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = mergeDeepLeft(x, y)) // {"a":{"a":1,"b":1}}

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

Prueba de unidad:

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

Encontré solo una solución de 2 líneas para obtener una fusión profunda en javascript. Déjame saber cómo funciona esto para ti.

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)

El objeto temporal imprimirá {a: {b: 'd', e: 'f', x: 'y'}}


1
Esto no hace una fusión profunda real. Fallará con merge({x:{y:{z:1}}}, {x:{y:{w:2}}}). Il tampoco podrá actualizar los valores existentes en obj1 si obj2 también los tiene, por ejemplo con merge({x:{y:1}}, {x:{y:2}}).
Oreilles

1

A veces no necesita una fusión profunda, incluso si lo cree así. Por ejemplo, si tiene una configuración predeterminada con objetos anidados y desea ampliarla profundamente con su propia configuración, puede crear una clase para eso. El concepto es muy simple:

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

Puede convertirlo en una función (no en un constructor).


1

Esta es una fusión profunda y barata que utiliza el mínimo código posible. Cada fuente sobrescribe la propiedad anterior cuando existe.

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

1

Estoy usando la siguiente función corta para fusionar objetos en profundidad.
Funciona muy bien para mí.
El autor explica completamente cómo funciona aquí.

/*!
 * 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;

};

Si bien este enlace puede responder la pregunta, es mejor incluir las partes esenciales de la respuesta aquí y proporcionar el enlace como referencia. Las respuestas de solo enlace pueden volverse inválidas si la página vinculada cambia. - De la opinión
Chris Camaratta

Hola @ChrisCamaratta. Aquí no solo está la parte esencial, está todo aquí: la función y cómo usarla. Así que definitivamente esta no es una respuesta de solo enlace. Esta es la función que he estado usando para fusionar objetos en profundidad. El enlace es solo si desea que los autores expliquen cómo funciona. Creo que sería un mal servicio para la comunidad tratar de explicar el funcionamiento mejor que el autor que enseña JavaScript. Gracias por el comentario.
John Shearing

Huh O lo perdí o el código no apareció en la interfaz del revisor cuando lo revisé. Estoy de acuerdo en que esta es una respuesta de calidad. Parece que otros revisores anularon mi evaluación inicial, así que creo que estás bien. Perdón por la bandera de inspiración.
Chris Camaratta

¡Excelente! @ ChrisCamaratta, gracias por ayudarme a entender lo que pasó.
John Shearing
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.