Como mesclagem profunda em vez de mesclagem superficial?

337

Ambos Object.assign e objeto propagação só fazem uma fusão superficial.

Um exemplo do problema:

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

A saída é o que você esperaria. No entanto, se eu tentar isso:

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

Ao invés de

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

você recebe

{ a: { b: 1 } }

x é completamente sobrescrito porque a sintaxe de dispersão atinge apenas um nível de profundidade. É o mesmo com Object.assign().

Existe uma maneira de fazer isso?

Mike
fonte
é profunda mesclar o mesmo que copiar propriedades de um objeto para outro?
2
Não, como as propriedades do objeto não devem ser substituídas, cada objeto filho deve ser mesclado no mesmo filho no destino, se ele já existir.
Mike
O ES6 está finalizado e os novos recursos não são mais adicionados, AFAIK.
kangax
11
@Oriol exige jQuery embora ...
m0meni

Respostas:

330

Alguém sabe se existe uma fusão profunda nas especificações do ES6 / ES7?

Não, não tem.


fonte
21
Revise o histórico de edições. No momento em que respondi, a pergunta era : Alguém sabe se existe uma fusão profunda nas especificações do ES6 / ES7? .
37
Esta resposta já não se aplica a esta pergunta - que deve ser atualizado ou excluído
DonVaughn
13
A questão não deveria ter sido editada para este grau. As edições são para esclarecer. Uma nova pergunta deveria ter sido postada.
CJ Thompson
170

Sei que esse é um problema antigo, mas a solução mais fácil no ES2015 / ES6 que eu pude encontrar foi realmente bastante simples, usando Object.assign (),

Espero que isso ajude:

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

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

Você encontrará uma versão imutável disso na resposta abaixo.

Observe que isso levará a uma recursão infinita em referências circulares. Aqui estão algumas ótimas respostas sobre como detectar referências circulares, se você acha que enfrentaria esse problema.

Salakar
fonte
11
se o seu gráfico de objeto contém ciclos que levará a recursividade infinita
the8472
2
Por que escrever isto: Object.assign(target, { [key]: {} })se pudesse simplesmente ser target[key] = {}?
Jürg Lehni 25/10
11
... e em target[key] = source[key]vez deObject.assign(target, { [key]: source[key] });
Jürg Lehni 25/10
3
Isso não suporta objetos não comuns no target. Por exemplo, mergeDeep({a: 3}, {a: {b: 4}})resultará em um Numberobjeto aumentado , o que claramente não é desejado. Além disso, isObjectnão aceita matrizes, mas aceita qualquer outro tipo de objeto nativo, como Date, que não deve ser copiado em profundidade.
riv
2
Não funciona com matrizes como eu entendo?
Vedmant 13/08/19
119

Você pode usar a mesclagem 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 }] }
AndrewHenderson
fonte
6
Ei pessoal, esta é a solução mais simples e bonita. Lodash é incrível, eles devem incluí-lo como núcleo js objeto
Nurbol Alpysbayev
11
O resultado não deveria ser { 'a': [{ 'b': 2 }, { 'c': 3 }, { 'd': 4 }, { 'e': 5 }] }?
J. Hesters
Boa pergunta. Essa pode ser uma pergunta separada ou uma para os mantenedores da Lodash.
AndrewHenderson
7
O resultado { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }está correto, porque estamos mesclando elementos de uma matriz. O elemento 0de object.aé {b: 2}, o elemento 0de other.aé {c: 3}. Quando esses dois são mesclados porque têm o mesmo índice de matriz, o resultado é { 'b': 2, 'c': 3 }: qual é o elemento 0no novo objeto.
Alexandru Furculita
Eu prefiro este , é 6x menor compactado.
Solo
101

O problema não é trivial quando se trata de hospedar objetos ou qualquer tipo de objeto mais complexo que um conjunto de valores

  • você chama um getter para obter um valor ou copia sobre o descritor de propriedades?
  • e se o destino de mesclagem tiver um setter (propriedade própria ou em sua cadeia de protótipos)? Você considera o valor como já presente ou chama o setter para atualizar o valor atual?
  • você invoca funções de propriedade própria ou as copia? E se elas forem funções vinculadas ou funções de seta, dependendo de algo em sua cadeia de escopo no momento em que foram definidas?
  • e se for algo como um nó DOM? Você certamente não deseja tratá-lo como objeto simples e mesclar profundamente todas as suas propriedades em
  • como lidar com estruturas "simples", como matrizes, mapas ou conjuntos? Considere-os já presentes ou mesclá-los também?
  • como lidar com propriedades próprias não enumeráveis?
  • e as novas subárvores? Basta atribuir por referência ou clone profundo?
  • como lidar com objetos congelados / selados / não extensíveis?

Outra coisa a ter em mente: gráficos de objetos que contêm ciclos. Geralmente não é difícil lidar com isso - basta manter umSet dos objetos de origem já visitados - mas muitas vezes esquecidos.

Você provavelmente deve escrever uma função de mesclagem profunda que espera apenas valores primitivos e objetos simples - no máximo aqueles tipos que o algoritmo de clone estruturado pode manipular - como fontes de mesclagem. Jogue se encontrar algo que não possa manipular ou apenas atribuir por referência, em vez de uma fusão profunda.

Em outras palavras, não existe um algoritmo de tamanho único, você precisa criar o seu próprio ou procurar um método de biblioteca que cubra seus casos de uso.

the8472
fonte
2
desculpas para V8 devs para não implementar um seguro de transferência de "estado de documentos"
neaumusic
Você levantou muitas questões boas e eu adoraria ver uma implementação de sua recomendação. Então eu tentei fazer um abaixo. Você poderia dar uma olhada e comentar? stackoverflow.com/a/48579540/8122487
RaphaMex 03/02
66

Aqui está uma versão imutável (não modifica as entradas) da resposta de @ Salakar. Útil se você estiver fazendo coisas funcionais do tipo programação.

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;
}
CpILL
fonte
11
@torazaburo ver post anterior por mim para a função IsObject
Salakar
atualizado. depois de alguns testes eu encontrei um bug com os objetos profundamente aninhados
CpILL
3
É um nome de propriedade calculado, o primeiro usará o valor de keycomo o nome da propriedade, o posterior fará "key" o nome da propriedade. Veja: es6-features.org/#ComputedPropertyNames
CpILL
2
em que isObjectvocê não precisa checar && item !== nullno final, porque a linha começa com item &&, não?
Ephemer 14/11/16
2
Se a origem aninhou objetos filhos mais profundamente que o destino, esses objetos ainda farão referência aos mesmos valores na mergedDeepsaída (acho). Por exemplo, 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 isso é um problema? Ele não modifica as entradas, mas quaisquer futuras mutações nas entradas podem alterar a saída e vice-versa com mutações na saída de entradas mutantes. Por quanto vale, porém, o ramda's R.merge()tem o mesmo comportamento.
James Conkling,
40

Como esse problema ainda está ativo, aqui está outra abordagem:

  • ES6 / 2015
  • Imutável (não modifica objetos originais)
  • Manipula matrizes (concatena-as)

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

jhildenbiddle
fonte
Isso é legal. No entanto, quando temos um array com elementos repetidos, eles são concatenados (existem elementos repetidos). Eu adaptei isso para pegar um parâmetro (matrizes únicas: verdadeiro / falso).
Astronauta
11
Para fazer as matrizes único que você pode mudar prev[key] = pVal.concat(...oVal);paraprev[key] = [...pVal, ...oVal].filter((element, index, array) => array.indexOf(element) === index);
Richard Herries
11
Tão agradável e limpo !! Definitivamente a melhor resposta aqui!
experiência
Glorioso. Este também demonstra que as matrizes são mescladas, o que eu estava procurando.
Tschallacka 13/11/19
Sim, é dito que a solução @CplLL é imutável, mas usa mutabilidade de objeto real dentro da função enquanto o uso reduce não.
Augustin Riedinger
30

Eu sei que já existem muitas respostas e tantos comentários argumentando que eles não vão funcionar. O único consenso é que é tão complicado que ninguém fez um padrão para isso . No entanto, a maioria das respostas aceitas no SO expõe "truques simples" amplamente usados. Portanto, para todos nós, como eu, que não somos especialistas, mas queremos escrever código mais seguro, compreendendo um pouco mais sobre a complexidade do javascript, tentarei esclarecer um pouco.

Antes de sujar as mãos, deixe-me esclarecer 2 pontos:

  • [AVISO LEGAL] Proponho uma função abaixo que aborda como fazemos um loop profundo em objetos javascript para cópia e ilustra o que geralmente é comentado em pouco tempo. Não está pronto para produção. Por uma questão de clareza, propositadamente deixei de lado outras considerações, como objetos circulares (rastrear por um conjunto ou propriedade de símbolo não conflitante) , copiar valor de referência ou clone profundo , objeto de destino imutável (clone profundo novamente?), Estudo caso a caso de cada tipo de objeto obtém / define propriedades por meio de acessadores ... Além disso, não testei o desempenho - embora seja importante - porque também não é o ponto aqui.
  • Usarei copiar ou atribuir termos em vez de mesclar . Porque, na minha opinião, uma fusão é conservadora e deve falhar em conflitos. Aqui, quando conflitantes, queremos que a fonte substitua o destino. Como Object.assignfaz.

Respostas com for..inou Object.keyssão enganosas

Fazer uma cópia profunda parece uma prática tão básica e comum que esperamos encontrar uma única linha ou, pelo menos, uma vitória rápida por meio de recursão simples. Não esperamos precisar de uma biblioteca ou escrever uma função personalizada de 100 linhas.

Quando li pela primeira vez a resposta de Salakar , realmente pensei que poderia fazer melhor e mais simples (você pode compará-la com Object.assignon x={a:1}, y={a:{b:1}}). Então li a resposta do the8472 e pensei ... não há como fugir tão facilmente, melhorar as respostas já dadas não nos levará longe.

Vamos deixar uma cópia profunda e recursiva de lado por um instante. Considere como as pessoas (erradamente) analisam propriedades para copiar um objeto muito simples.

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á propriedades não enumeráveis ​​próprias, propriedades com chave de símbolo e todas as propriedades do protótipo. Pode ser bom se seus objetos não tiverem nenhum deles. Mas lembre-se de que Object.assignlida com propriedades enumeráveis ​​com chave de símbolo. Portanto, sua cópia personalizada perdeu a flor.

for..infornecerá propriedades da fonte, de seu protótipo e de toda a cadeia de protótipos sem que você queira (ou saiba). Seu destino pode acabar com muitas propriedades, misturando propriedades de protótipo e propriedades próprias.

Se você estiver escrevendo uma função de propósito geral e você não está usando Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames, Object.getOwnPropertySymbolsou Object.getPrototypeOf, você está provavelmente fazendo errado.

Pontos a considerar antes de escrever sua função

Primeiro, certifique-se de entender o que é um objeto Javascript. Em Javascript, um objeto é feito com suas próprias propriedades e um objeto de protótipo (pai). O objeto protótipo, por sua vez, é feito de suas próprias propriedades e um objeto protótipo. E assim por diante, definindo uma cadeia de protótipos.

Uma propriedade é um par de chave ( stringou symbol) e descritor ( valueou get/ setacessador e atributos como enumerable).

Finalmente, existem muitos tipos de objetos . Convém manipular de maneira diferente um objeto Objeto de um objeto Data ou um objeto Função.

Portanto, ao escrever sua cópia detalhada, você deve responder pelo menos a essas perguntas:

  1. O que considero profundo (adequado para pesquisa recursiva) ou plano?
  2. Quais propriedades eu quero copiar? (enumerável / não enumerável, com chave de cadeia / com chave de símbolo, propriedades próprias / próprias propriedades do protótipo, valores / descritores ...)

Para o meu exemplo, considero que apenas os object Objectsão profundos , porque outros objetos criados por outros construtores podem não ser adequados para uma análise aprofundada. Personalizado a partir deste 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 fiz um optionsobjeto para escolher o que copiar (para fins de demonstração).

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

Função proposta

Você pode testá-lo neste 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;
    }
}

Isso pode ser usado assim:

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

Eu uso o lodash:

import _ = require('lodash');
value = _.merge(value1, value2);
Jeff Tian
fonte
2
Note-se que merge vai alterar objeto, se você quiser algo que não transformar o objeto, então _cloneDeep(value1).merge(value2)
lagartixas
3
@geckos Você pode fazer _.merge ({}, value1, value2)
Spenhouet 12/11/19
10

Aqui está a implementação do 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);
};

E testes de unidade:

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*/}
}
am0wa
fonte
8

Gostaria de apresentar uma alternativa ES5 bastante simples. A função obtém 2 parâmetros - targete sourceisso deve ser do tipo "objeto". Targetserá o objeto resultante. Targetmantém todas as suas propriedades originais, mas seus valores podem ser modificados.

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:

  • se targetnão tem uma sourcepropriedade, targetobtém-a;
  • E se target possui uma sourcepropriedade e target& sourcenão são ambos objetos (3 casos em 4), targeta propriedade de é substituída;
  • se targetpossui uma sourcepropriedade e ambos são objetos / matrizes (1 caso restante), a recursão ocorre mesclando dois objetos (ou concatenação de duas matrizes);

considere também o seguinte :

  1. matriz + obj = matriz
  2. obj + array = obj
  3. obj + obj = obj (mesclado recursivamente)
  4. matriz + matriz = matriz (concat)

É previsível, suporta tipos primitivos, bem como matrizes e objetos. Além disso, como podemos mesclar 2 objetos, acho que podemos mesclar mais de 2 via função de redução .

dê uma olhada em um exemplo (e brinque com ele, se quiser) :

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

Há uma limitação - o comprimento da pilha de chamadas do navegador. Navegadores modernos lançam um erro em um nível realmente profundo de recursão (pense em milhares de chamadas aninhadas). Além disso, você é livre para tratar situações como array + objeto etc., como desejar, adicionando novas condições e verificações de tipo.

bola curva
fonte
8

Aqui está outra solução ES6, funciona com objetos e matrizes.

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
}
pravdomil
fonte
3
é testado e / ou parte de uma biblioteca, parece bom, mas gostaria de ter certeza de que é um pouco comprovado.
7

Se você estiver usando o ImmutableJS, poderá usar mergeDeep:

fromJS(options).mergeDeep(options2).toJS();
Dimitri Kopriwa
fonte
2
@EliseChant Acho que não. Por que você não esclarece?
precisa saber é o seguinte
7

Existe uma maneira de fazer isso?

Se as bibliotecas npm puderem ser usadas como uma solução, o avançado de mesclagem de objetos da sua verdadeiramente permitirá mesclar objetos profundamente e personalizar / substituir cada ação de mesclagem usando uma função familiar de retorno de chamada. A idéia principal é mais do que uma fusão profunda - o que acontece com o valor quando duas chaves são iguais ? Essa biblioteca cuida disso - quando duas chaves se chocam, object-merge-advancedpesa os tipos, com o objetivo de reter o máximo de dados possível após a mesclagem:

chave de objeto mesclando os tipos de valor-chave de pesagem para reter o máximo de dados possível

A chave do primeiro argumento de entrada está marcada como # 1, e o segundo argumento - # 2. Dependendo de cada tipo, um é escolhido para o valor da chave do resultado. No diagrama, "um objeto" significa um objeto simples (não matriz etc.).

Quando as teclas não se chocam, todas elas inserem o resultado.

No seu snippet de exemplo, se você costumava object-merge-advancedmesclar seu snippet 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
//      }
//    }

Seu algoritmo percorre recursivamente todas as chaves de objetos de entrada, compara e constrói e retorna o novo resultado mesclado.

regozijar-se
fonte
6

A função a seguir faz uma cópia profunda dos objetos, abrange cópias primitivas, matrizes e 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;
}
sudharsan tk
fonte
6

Uma solução simples com o ES5 (substitua o 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));

yc
fonte
apenas o que eu precisava - ES6 estava causando problemas na construção - esta alternativa ES5 é a bomba
danday74
5

Muitos exemplos aqui parecem muito complexos, estou usando um no TypeScript que criei, acho que deve cobrir a maioria dos casos (estou lidando com matrizes como dados regulares, apenas substituindo-as).

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

A mesma coisa na JS simples, apenas no caso:

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

Aqui estão meus casos de teste para mostrar como você pode usá-lo

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

Entre em contato se achar que estou faltando alguma funcionalidade.

Ezequiel
fonte
5

Se você deseja ter um liner sem precisar de uma biblioteca enorme como o lodash, sugiro que você use o deepmerge . ( npm install deepmerge)

Então você pode fazer

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

para obter

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

O bom é que ele vem com digitações para o TypeScript imediatamente. Também permite mesclar matrizes . Uma verdadeira solução completa é essa.

Martin Braun
fonte
4

Podemos usar $ .extend (true, objeto1, objeto2) para mesclagem profunda. Valor true denota mesclar dois objetos recursivamente, modificando o primeiro.

$ extend (verdadeiro, alvo, objeto)

Abinaya
fonte
9
O solicitante nunca indicou que está usando jquery e parece estar solicitando uma solução javascript nativa.
TeJ JoE
Essa é uma maneira muito simples de fazer isso e funciona. Uma solução viável que eu consideraria se fosse eu quem fizesse essa pergunta. :)
kashiraja
Esta é uma resposta muito boa, mas está faltando um link para o código-fonte do jQuery. O jQuery tem muitas pessoas trabalhando no projeto e elas gastaram algum tempo fazendo cópias profundas trabalhando corretamente. Além disso, o código-fonte é bastante "simples": github.com/jquery/jquery/blob/master/src/core.js#L125 "Simples" está entre aspas porque começa a ficar complicado ao se aprofundar jQuery.isPlainObject(). Isso expõe a complexidade de determinar se algo é ou não um objeto simples, que a maioria das respostas aqui deixa de lado. Adivinha em que idioma o jQuery está escrito?
CubicleSoft 26/03/19
4

Aqui, a solução simples e direta que funciona como Object.assignapenas adiar e trabalhar para uma matriz, sem nenhuma modificação

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

Exemplo

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
}

pery mimon
fonte
3

Eu estava tendo esse problema ao carregar um estado redux em cache. Se eu apenas carregar o estado em cache, ocorrerá erros na nova versão do aplicativo com uma estrutura de estado atualizada.

Já foi mencionado que o lodash oferece a mergefunção que eu usei:

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

Muitas respostas usam dezenas de linhas de código ou exigem a adição de uma nova biblioteca ao projeto, mas se você usar recursão, serão apenas quatro linhas 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 } }));

Manipulação de matrizes: a versão acima substitui os valores antigos de matriz por novos. Se você deseja manter os valores antigos da matriz e adicionar os novos, basta adicionar um else if (current[key] instanceof Array && updates[key] instanceof Array) current[key] = current[key].concat(updates[key])bloco acima do elseestatamento e está tudo pronto.

Vincent
fonte
11
Eu gosto, mas ele precisa de uma verificação indefinida simples para 'current' ou então {foo: undefined} não se mescla. Basta adicionar um if (atual) antes do loop for.
Andreas Pardeike
Obrigado pela sugestão
Vincent
2

Aqui está outro que acabei de escrever que suporta matrizes. Concats eles.

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);
};
mpen
fonte
2

Use esta função:

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;
    }
Vikram Biwal
fonte
2

Ramda, que é uma boa biblioteca de funções javascript, possui mergeDeepLeft e mergeDeepRight. Qualquer uma dessas funciona muito bem para esse problema. Por favor, dê uma olhada na documentação aqui: https://ramdajs.com/docs/#mergeDeepLeft

Para o exemplo específico em questão, 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}}
afonte
fonte
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;
}

Teste de unidade:

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,
        },
      });
    });
  });
Sergey Gurin
fonte
2

Eu encontrei apenas solução de 2 linhas para obter uma fusão profunda em javascript. Deixe-me saber como isso funciona para você.

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)

O objeto Temp imprimirá {a: {b: 'd', e: 'f', x: 'y'}}

saumilsdk
fonte
11
Isso não faz uma mesclagem profunda real. Falhará com merge({x:{y:{z:1}}}, {x:{y:{w:2}}}). Também não será possível atualizar os valores existentes no obj1 se o obj2 também os tiver, por exemplo, com merge({x:{y:1}}, {x:{y:2}}).
Oreilles
1

Às vezes, você não precisa de mesclagem profunda, mesmo que pense assim. Por exemplo, se você possui uma configuração padrão com objetos aninhados e deseja estendê-la profundamente com sua própria configuração, pode criar uma classe para isso. O conceito é muito simples:

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

Você pode convertê-lo em uma função (não um construtor).

Ruslan
fonte
1

Essa é uma mesclagem profunda barata que usa o mínimo de código que eu poderia imaginar. Cada fonte substitui a propriedade anterior quando ela 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 }));
Lewis
fonte
1

Estou usando a seguinte função curta para mesclar objetos profundamente.
Funciona muito bem para mim.
O autor explica completamente como funciona aqui.

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

};
John Shearing
fonte
Embora esse link possa responder à pergunta, é melhor incluir aqui as partes essenciais da resposta e fornecer o link para referência. As respostas somente para links podem se tornar inválidas se a página vinculada for alterada. - Da avaliação
Chris Camaratta
Oi @ChrisCamaratta. Não é apenas a parte essencial aqui, está tudo aqui - a função e como usá-la. Portanto, essa definitivamente não é apenas uma resposta para o link. Esta é a função que tenho usado para mesclar objetos em profundidade. O link é apenas se você quiser que os autores expliquem como ele funciona. Acho que seria um desserviço para a comunidade tentar explicar melhor o funcionamento do que o autor que ensina JavaScript. Obrigado pelo comentário.
John Shearing
Hã. Perdi ou o código não apareceu na interface do revisor quando o revisei. Concordo que esta é uma resposta de qualidade. Parece que outros revisores substituíram minha avaliação inicial, então acho que você está bem. Desculpe pela bandeira de inspiração.
Chris Camaratta
Ótimo! @ ChrisCamaratta, Obrigado por me ajudar a entender o que aconteceu.
John Shearing