Comment fusionner en profondeur au lieu de fusion superficielle?

340

Les deux Object.assign et la propagation objet que de faire une fusion peu profonde.

Un exemple du problème:

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

La sortie est ce que vous attendez. Cependant, si j'essaye ceci:

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

Au lieu de

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

vous obtenez

{ a: { b: 1 } }

x est complètement écrasé car la syntaxe de propagation ne va que d'un niveau. C'est la même chose avec Object.assign().

Y a-t-il un moyen de faire cela?

Mike
la source
la fusion en profondeur est-elle la même que la copie de propriétés d'un objet à un autre?
2
Non, car les propriétés d'objet ne doivent pas être écrasées, mais chaque objet enfant doit être fusionné dans le même enfant sur la cible s'il existe déjà.
Mike
ES6 est finalisé et de nouvelles fonctionnalités ne sont plus ajoutées, AFAIK.
kangax
1
@Oriol nécessite cependant jQuery ...
m0meni

Réponses:

331

Quelqu'un sait-il si une fusion profonde existe dans la spécification ES6 / ES7?

Non.


la source
21
Veuillez consulter l'historique des modifications. Au moment où j'ai répondu à cela, la question était: Est-ce que quelqu'un sait s'il existe une fusion profonde dans la spécification ES6 / ES7? .
37
Cette réponse ne s'applique plus à cette question - elle doit être mise à jour ou supprimée
DonVaughn
13
La question n'aurait pas dû être modifiée à ce point. Les modifications visent à clarifier. Une nouvelle question aurait dû être publiée.
CJ Thompson
171

Je sais que c'est un peu un vieux problème mais la solution la plus simple dans ES2015 / ES6 que j'ai pu trouver était en fait assez simple, en utilisant Object.assign (),

Espérons que cela aide:

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

Exemple d'utilisation:

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

Vous en trouverez une version immuable dans la réponse ci-dessous.

Notez que cela conduira à une récursion infinie sur les références circulaires. Il existe d'excellentes réponses ici sur la façon de détecter les références circulaires si vous pensez que vous seriez confronté à ce problème.

Salakar
la source
1
si votre graphe objet contient des cycles qui mèneront à une récursion infinie
the8472
item !== nullne devrait pas être nécessaire à l'intérieur isObject, car la itemvéracité est déjà vérifiée au début de la condition
mcont
2
Pourquoi écrire ceci: Object.assign(target, { [key]: {} })si cela pouvait être simplement target[key] = {}?
Jürg Lehni
1
... et target[key] = source[key]au lieu deObject.assign(target, { [key]: source[key] });
Jürg Lehni
3
Cela ne prend en charge aucun objet non ordinaire dans target. Par exemple, mergeDeep({a: 3}, {a: {b: 4}})se traduira par un Numberobjet augmenté , ce qui n'est clairement pas souhaité. En outre, isObjectn'accepte pas les tableaux, mais accepte tout autre type d'objet natif, tel que Date, qui ne doit pas être copié en profondeur.
riv
122

Vous pouvez utiliser la fusion 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
la source
6
Hé les gens, c'est la solution la plus simple et la plus belle. Lodash est génial, ils devraient l'inclure en tant qu'objet js de base
Nurbol Alpysbayev
11
Le résultat ne devrait-il pas être { 'a': [{ 'b': 2 }, { 'c': 3 }, { 'd': 4 }, { 'e': 5 }] }?
J. Hesters
Bonne question. Cela pourrait être une question distincte ou une question pour les responsables de Lodash.
AndrewHenderson
7
Le résultat { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }est correct, car nous fusionnons les éléments d'un tableau. L'élément 0de object.aest {b: 2}, l'élément 0de other.aest {c: 3}. Lorsque ces deux sont fusionnés car ils ont le même index de tableau, le résultat est { 'b': 2, 'c': 3 }, qui est l'élément 0dans le nouvel objet.
Alexandru Furculita
Je préfère celui-ci , il est 6x plus petit.
Solo
101

Le problème n'est pas trivial lorsqu'il s'agit d'héberger des objets ou tout type d'objet plus complexe qu'un sac de valeurs

  • invoquez-vous un getter pour obtenir une valeur ou copiez-vous le descripteur de propriété?
  • que se passe-t-il si la cible de fusion a un setter (soit sa propre propriété, soit sa chaîne de prototypes)? Considérez-vous la valeur comme déjà présente ou appelez le setter pour mettre à jour la valeur actuelle?
  • invoquez-vous des fonctions propres ou les copiez-vous? Et si ce sont des fonctions liées ou des fonctions fléchées en fonction de quelque chose dans leur chaîne de portée au moment où elles ont été définies?
  • Et si c'est quelque chose comme un nœud DOM? Vous ne voulez certainement pas le traiter comme un objet simple et simplement fusionner en profondeur toutes ses propriétés dans
  • comment gérer des structures "simples" comme des tableaux, des cartes ou des ensembles? Les considérer comme déjà présents ou les fusionner aussi?
  • comment gérer les propriétés propres non énumérables?
  • qu'en est-il des nouveaux sous-arbres? Attribuer simplement par référence ou clone profond?
  • comment gérer les objets gelés / scellés / non extensibles?

Une autre chose à garder à l'esprit: les graphes d'objets qui contiennent des cycles. Il n'est généralement pas difficile à gérer - conservez simplement un Setdes objets source déjà visités - mais souvent oublié.

Vous devriez probablement écrire une fonction de fusion profonde qui n'attend que des valeurs primitives et des objets simples - tout au plus les types que l' algorithme de clone structuré peut gérer - comme sources de fusion. Lancer s'il rencontre quelque chose qu'il ne peut pas gérer ou simplement attribuer par référence au lieu d'une fusion en profondeur.

En d'autres termes, il n'y a pas d'algorithme unique, vous devez soit lancer le vôtre, soit rechercher une méthode de bibliothèque qui couvre vos cas d'utilisation.

the8472
la source
2
excuses pour que les développeurs de V8 ne mettent pas en œuvre un transfert sécurisé de "l'état du document"
neaumusic
Vous soulevez de nombreuses bonnes questions et j'aurais aimé voir une mise en œuvre de votre recommandation. J'ai donc essayé d'en faire un ci-dessous. Pourriez-vous s'il vous plaît jeter un œil et commenter? stackoverflow.com/a/48579540/8122487
RaphaMex
66

Voici une version immuable (ne modifie pas les entrées) de la réponse de @ Salakar. Utile si vous faites des trucs de type programmation fonctionnelle.

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
la source
1
@torazaburo voir le post précédent de moi pour la fonction
isObject
l'a mis à jour. après quelques tests, j'ai trouvé un bug avec les objets profondément imbriqués
CpILL
3
C'est un nom de propriété calculé, le premier utilisera la valeur de keycomme nom de propriété, le second fera "clé" le nom de la propriété. Voir: es6-features.org/#ComputedPropertyNames
CpILL
2
en isObjectvous n'avez pas besoin de vérifier && item !== nullà la fin, parce que la ligne commence avec item &&, non?
éphémère
2
Si la source a imbriqué des objets enfants plus profondément que la cible, ces objets feront toujours référence aux mêmes valeurs dans mergedDeepla sortie de (je pense). Par exemple, 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 est-ce un problème? Il ne mute pas les entrées, mais toute mutation future des entrées pourrait muter la sortie, et vice versa avec des mutations pour sortir les entrées mutantes. Pour ce que ça vaut, cependant, ramda R.merge()a le même comportement.
James Conkling
40

Étant donné que ce problème est toujours actif, voici une autre approche:

  • ES6 / 2015
  • Immuable (ne modifie pas les objets originaux)
  • Gère les tableaux (les concatène)

/**
* 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
la source
C'est sympa. Cependant, lorsque nous avons un tableau avec des éléments répétés, ceux-ci sont concaténés (il existe des éléments répétés). J'ai adapté cela pour prendre un paramètre (tableaux uniques: vrai / faux).
Astronaut
1
Pour rendre les tableaux uniques, vous pouvez passer prev[key] = pVal.concat(...oVal);àprev[key] = [...pVal, ...oVal].filter((element, index, array) => array.indexOf(element) === index);
Richard Herries
1
Tellement agréable et propre !! Certainement la meilleure réponse ici!
538ROMEO
Glorieux. Celui-ci démontre également que les tableaux sont fusionnés, ce que je cherchais.
Tschallacka
Oui, la solution @CplLL est considérée comme immuable, mais utilise la mutabilité réelle des objets à l'intérieur de la fonction alors que ce reduce n'est pas le cas.
Augustin Riedinger
30

Je sais qu'il y a déjà beaucoup de réponses et autant de commentaires soutenant qu'ils ne fonctionneront pas. Le seul consensus est que c'est tellement compliqué que personne n'a fait de norme pour cela . Cependant, la plupart des réponses acceptées dans SO exposent des «astuces simples» qui sont largement utilisées. Donc, pour nous tous comme moi qui ne sommes pas des experts mais qui veulent écrire du code plus sûr en saisissant un peu plus la complexité de javascript, je vais essayer de faire la lumière.

Avant de nous salir les mains, permettez-moi de clarifier 2 points:

  • [AVERTISSEMENT] Je propose ci-dessous une fonction qui aborde la façon dont nous bouclons en profondeur dans les objets javascript pour copie et illustre ce qui est généralement trop brièvement commenté. Il n'est pas prêt pour la production. Par souci de clarté, j'ai délibérément laissé de côté d'autres considérations comme les objets circulaires (suivre par un ensemble ou une propriété de symbole non conflictuel) , copier la valeur de référence ou le clone profond , objet de destination immuable (cloner à nouveau?), Étude au cas par cas de chaque type d'objets , obtenir / définir des propriétés via des accesseurs ... De plus, je n'ai pas testé les performances - bien que ce soit important - car ce n'est pas le point ici non plus.
  • Je vais utiliser la copie ou attribuer des termes au lieu de fusionner . Parce que dans mon esprit, une fusion est conservatrice et devrait échouer lors de conflits. Ici, en cas de conflit, nous voulons que la source écrase la destination. Comme Object.assignça.

Réponses avec for..inou Object.keystrompeuses

Faire une copie en profondeur semble une pratique tellement basique et courante que nous nous attendons à trouver un one-liner ou, au moins, une victoire rapide via une simple récursivité. Nous ne nous attendons pas à ce que nous ayons besoin d'une bibliothèque ou d'écrire une fonction personnalisée de 100 lignes.

Quand j'ai lu la réponse de Salakar pour la première fois , j'ai vraiment pensé que je pouvais faire mieux et plus simplement (vous pouvez le comparer avec Object.assignon x={a:1}, y={a:{b:1}}). Puis j'ai lu la réponse du 8447 et j'ai pensé ... qu'il n'y a pas de moyen de s'échapper si facilement, l'amélioration des réponses déjà données ne nous mènera pas loin.

Laissons la copie profonde et récursive de côté un instant. Considérez simplement comment (à tort) les gens analysent les propriétés pour copier un objet très 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.keysomettra ses propres propriétés non énumérables, ses propres propriétés à clé de symbole et toutes les propriétés du prototype. Cela peut être bien si vos objets n'en ont pas. Mais gardez à l'esprit qu'il Object.assigngère ses propres propriétés énumérables à clé. Votre copie personnalisée a donc perdu son éclat.

for..infournira les propriétés de la source, de son prototype et de la chaîne complète de prototypes sans que vous le vouliez (ou que vous le sachiez). Votre cible peut se retrouver avec trop de propriétés, mélangeant des propriétés de prototype et des propriétés propres.

Si vous écrivez une fonction d'usage général et vous ne l' utilisez Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames, Object.getOwnPropertySymbolsou Object.getPrototypeOf, vous faites sans doute le plus mal.

Éléments à considérer avant d'écrire votre fonction

Tout d'abord, assurez-vous de comprendre ce qu'est un objet Javascript. En Javascript, un objet est composé de ses propres propriétés et d'un objet prototype (parent). L'objet prototype est à son tour constitué de ses propres propriétés et d'un objet prototype. Et ainsi de suite, définir une chaîne prototype.

Une propriété est une paire de clé ( stringou symbol) et de descripteur ( valueou get/ setaccesseur, et des attributs comme enumerable).

Enfin, il existe de nombreux types d'objets . Vous souhaiterez peut-être gérer différemment un objet Object à partir d'un objet Date ou d'un objet Function.

Donc, en écrivant votre copie complète, vous devez répondre au moins à ces questions:

  1. Qu'est-ce que je considère profond (approprié pour une recherche récursive) ou plat?
  2. Quelles propriétés dois-je copier? (énumérable / non énumérable, à chaîne / à symbole, propres propriétés / propriétés du prototype, valeurs / descripteurs ...)

Pour mon exemple, je considère que seuls les object Objects sont profonds , car d'autres objets créés par d'autres constructeurs peuvent ne pas convenir à un examen approfondi. Personnalisé à partir de ce 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);
}

Et j'ai fait un optionsobjet pour choisir quoi copier (à des fins de démonstration).

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

Fonction proposée

Vous pouvez le tester dans ce 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;
    }
}

Cela peut être utilisé comme ceci:

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

J'utilise lodash:

import _ = require('lodash');
value = _.merge(value1, value2);
Jeff Tian
la source
2
Notez que la fusion modifiera l'objet, si vous voulez quelque chose qui ne mute pas l'objet, alors _cloneDeep(value1).merge(value2)
geckos
3
@geckos Vous pouvez faire _.merge ({}, value1, value2)
Spenhouet
10

Voici l'implémentation 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);
};

Et tests unitaires:

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
la source
9

Voici une autre solution ES6, qui fonctionne avec des objets et des tableaux.

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
la source
3
est-ce testé et / ou fait-il partie d'une bibliothèque?
8

Je voudrais présenter une alternative ES5 assez simple. La fonction obtient 2 paramètres - targetet sourcecela doit être de type "objet". Targetsera l'objet résultant. Targetconserve toutes ses propriétés d'origine mais leurs valeurs peuvent cependant être modifiées.

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

cas:

  • si targetn'a pas de sourcepropriété,target obtient;
  • si targeta une sourcepropriété et target& sourcene sont pas les deux objets (3 cas sur 4), targets'outrepassée la propriété »;
  • si targetpossède une sourcepropriété et que les deux sont des objets / tableaux (1 cas restant), alors la récursivité se produit en fusionnant deux objets (ou concaténation de deux tableaux);

tenez également compte des éléments suivants :

  1. tableau + obj = tableau
  2. obj + array = obj
  3. obj + obj = obj (fusionné récursivement)
  4. tableau + tableau = tableau (concat)

Il est prévisible, prend en charge les types primitifs ainsi que les tableaux et les objets. De plus, comme nous pouvons fusionner 2 objets, je pense que nous pouvons fusionner plus de 2 via la fonction de réduction .

jetez un oeil à un exemple (et jouez avec si vous voulez) :

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

Il existe une limitation - la longueur de la pile d'appels du navigateur. Les navigateurs modernes génèrent une erreur à un niveau de récursion très profond (pensez à des milliers d'appels imbriqués). Vous êtes également libre de traiter des situations comme tableau + objet, etc. comme vous le souhaitez en ajoutant de nouvelles conditions et en vérifiant le type.

boule de courbe
la source
7

Si vous utilisez ImmutableJS, vous pouvez utiliser mergeDeep:

fromJS(options).mergeDeep(options2).toJS();
Dimitri Kopriwa
la source
2
@EliseChant Je ne pense pas. Pourquoi ne clarifiez-vous pas?
Dimitri Kopriwa
7

Y a-t-il un moyen de faire cela?

Si les bibliothèques npm peuvent être utilisées comme solution, la fusion d'objets avancée de la vôtre permet vraiment de fusionner les objets en profondeur et de personnaliser / remplacer chaque action de fusion à l'aide d'une fonction de rappel familière. L'idée principale est plus qu'une simple fusion profonde - que se passe-t-il avec la valeur lorsque deux clés sont identiques ? Cette bibliothèque s'en charge - lorsque deux touches s'affrontent, object-merge-advancedpèse les types, dans le but de conserver autant de données que possible après la fusion:

fusion de clé d'objet types de valeurs de clé de pesée pour conserver autant de données que possible

La clé du premier argument d'entrée est marquée # 1, celle du deuxième argument - # 2. En fonction de chaque type, un est choisi pour la valeur de la clé de résultat. Dans le diagramme, "un objet" signifie un objet simple (pas un tableau, etc.).

Lorsque les touches ne s'affrontent pas, elles entrent toutes le résultat.

À partir de votre exemple d'extrait, si vous avez object-merge-advancedfusionné votre extrait de code:

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

Son algorithme traverse récursivement toutes les clés d'objet d'entrée, compare et construit et renvoie le nouveau résultat fusionné.

revelt
la source
6

La fonction suivante crée une copie complète des objets, elle couvre la copie de primitives, de tableaux et d'objets

 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
la source
6

Une solution simple avec ES5 (écraser la valeur existante):

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
la source
juste ce dont j'avais besoin - es6 causait des problèmes de construction - cette alternative es5 est la bombe
danday74
5

La plupart des exemples ici semblent trop complexes, j'en utilise un dans TypeScript que j'ai créé, je pense qu'il devrait couvrir la plupart des cas (je gère les tableaux comme des données normales, je les remplace simplement).

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

Même chose en clair JS, juste au cas où:

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

Voici mes cas de test pour montrer comment vous pouvez l'utiliser

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

Veuillez me faire savoir si vous pensez que je manque certaines fonctionnalités.

Ezequiel
la source
5

Si vous voulez avoir une seule ligne sans avoir besoin d'une immense bibliothèque comme lodash, je vous suggère d'utiliser deepmerge . ( npm install deepmerge)

Ensuite, vous pouvez faire

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

obtenir

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

La bonne chose est qu'il est livré avec des typages pour TypeScript tout de suite. Il permet également de fusionner des tableaux . C'est une vraie solution polyvalente.

Martin Braun
la source
4

Nous pouvons utiliser $ .extend (true, object1, object2) pour une fusion en profondeur. La valeur true indique la fusion récursive de deux objets, en modifiant le premier.

$ extend (vrai, cible, objet)

Abinaya
la source
9
Le demandeur n'a jamais indiqué qu'il utilise jquery et semble demander une solution javascript native.
Teh JoE
C'est une façon très simple de le faire et cela fonctionne. Une solution viable que j'envisagerais si c'était moi qui posais cette question. :)
kashiraja
C'est une très bonne réponse mais il manque un lien vers le code source vers jQuery. jQuery a beaucoup de gens qui travaillent sur le projet et ils ont passé un certain temps à faire fonctionner correctement la copie en profondeur. De plus, le code source est assez "simple": github.com/jquery/jquery/blob/master/src/core.js#L125 "Simple" est entre guillemets car il commence à se compliquer lors de la fouille jQuery.isPlainObject(). Cela expose la complexité de déterminer si quelque chose est un objet simple, ce que la plupart des réponses ici manquent de loin. Devinez dans quelle langue jQuery est écrit?
CubicleSoft
4

Voici une solution simple et directe qui fonctionne comme de la Object.assignprofondeur, et fonctionne pour un tableau, sans aucune modification

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

Exemple

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
la source
3

J'avais ce problème lors du chargement d'un état redux mis en cache. Si je charge simplement l'état mis en cache, je rencontrerais des erreurs pour la nouvelle version de l'application avec une structure d'état mise à jour.

Il a déjà été mentionné que lodash offre la mergefonction que j'ai utilisée:

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

De nombreuses réponses utilisent des dizaines de lignes de code ou nécessitent l'ajout d'une nouvelle bibliothèque au projet, mais si vous utilisez la récursivité, il ne s'agit que de 4 lignes de code.

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

Gestion des tableaux: la version ci-dessus remplace les anciennes valeurs de tableau par de nouvelles. Si vous voulez qu'il conserve les anciennes valeurs du tableau et en ajoute de nouvelles, ajoutez simplement un else if (current[key] instanceof Array && updates[key] instanceof Array) current[key] = current[key].concat(updates[key])bloc au-dessus du elsestatament et tout est prêt.

Vincent
la source
1
Je l'aime mais il a besoin d'une simple vérification non définie pour 'current' ou bien {foo: undefined} ne fusionne pas. Ajoutez simplement un if (actuel) avant la boucle for.
Andreas Pardeike
Merci pour la suggestion
Vincent
2

Voici un autre que je viens d'écrire qui prend en charge les tableaux. Il les concourt.

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
la source
2

Utilisez cette fonction:

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
la source
2

Ramda qui est une belle bibliothèque de fonctions javascript a mergeDeepLeft et mergeDeepRight. Tout cela fonctionne assez bien pour ce problème. Veuillez consulter la documentation ici: https://ramdajs.com/docs/#mergeDeepLeft

Pour l'exemple spécifique en question, nous pouvons utiliser:

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

Test de l'unité:

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
la source
2

J'ai trouvé seulement une solution en 2 lignes pour obtenir une fusion profonde en javascript. Faites-moi savoir comment cela fonctionne pour vous.

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)

L'objet temporaire affichera {a: {b: 'd', e: 'f', x: 'y'}}

saumilsdk
la source
1
Cela ne fait pas vraiment de fusion profonde. Il échouera avec merge({x:{y:{z:1}}}, {x:{y:{w:2}}}). Il échouera également à mettre à jour les valeurs existantes dans obj1 si obj2 les a aussi, par exemple avec merge({x:{y:1}}, {x:{y:2}}).
Oreilles
1

Parfois, vous n'avez pas besoin d'une fusion profonde, même si vous le pensez. Par exemple, si vous avez une configuration par défaut avec des objets imbriqués et que vous souhaitez l'étendre profondément avec votre propre configuration, vous pouvez créer une classe pour cela. Le concept est très 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);

Vous pouvez le convertir en fonction (pas en constructeur).

Ruslan
la source
1

Il s'agit d'une fusion en profondeur bon marché qui utilise le moins de code possible. Chaque source écrase la propriété précédente lorsqu'elle 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
la source
1

J'utilise la fonction courte suivante pour fusionner des objets en profondeur.
Ça marche bien pour moi.
L'auteur explique complètement comment cela fonctionne ici.

/*!
 * 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
la source
Bien que ce lien puisse répondre à la question, il est préférable d'inclure les parties essentielles de la réponse ici et de fournir le lien de référence. Les réponses de lien uniquement peuvent devenir invalides si la page liée change. - De l'avis
Chris Camaratta
Salut @ChrisCamaratta. Non seulement la partie essentielle ici, c'est tout ici - la fonction et comment l'utiliser. Ce n'est donc certainement pas une réponse de lien uniquement. C'est la fonction que j'ai utilisée pour fusionner en profondeur des objets. Le lien est uniquement si vous voulez que les auteurs expliquent comment cela fonctionne. Je pense que ce serait un mauvais service à la communauté d'essayer d'expliquer le fonctionnement mieux que l'auteur qui enseigne JavaScript. Merci pour le commentaire.
John Shearing
Huh. Soit je l'ai manqué, soit le code n'apparaissait pas dans l'interface du critique lorsque je l'ai examiné. Je suis d'accord que c'est une réponse de qualité. Il semblerait que d'autres commentateurs ont annulé mon évaluation initiale, donc je pense que vous allez bien. Désolé pour le drapeau d'inspiration.
Chris Camaratta
Génial! @ChrisCamaratta, Merci de m'avoir aidé à comprendre ce qui s'est passé.
John Shearing