web-dev-qa-db-fra.com

Comment faire une fusion profonde au lieu d'une fusion peu profonde?

Object.assign et Object spread ne font qu'une fusion superficielle.

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 qu'à un niveau. C'est la même chose avec Object.assign().

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

270
Mike

Est-ce que quelqu'un sait s'il existe une fusion profonde dans la spécification ES6/ES7?

Non.

297
user663031

Je sais que c’est un peu un vieux problème, mais la solution la plus simple que j’ai pu trouver dans ES2015/ES6 é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 trouverez une version immuable de cela 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 sur la façon de détecter les références circulaires si vous pensez faire face à ce problème.

143
Salakar

Le problème n'est pas trivial lorsqu'il s'agit d'objets Host ou de 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 objet de définition (propriété propre ou dans sa chaîne de prototypes)? Considérez-vous la valeur comme étant déjà présente ou appelez-vous le configurateur pour mettre à jour la valeur actuelle?
  • invoquez-vous des fonctions propres ou les copiez-vous? Que se passe-t-il si ce sont des fonctions liées ou des fonctions de flèche dépendant de quelque chose dans leur chaîne de scope au moment où elles ont été définies?
  • et si c'est quelque chose qui ressemble à un noeud DOM? Vous ne voulez certainement pas le traiter comme un simple objet et simplement fusionner toutes ses propriétés dans
  • comment traiter des structures "simples" comme des tableaux, des cartes ou des ensembles? Considérez-les déjà présents ou fusionnez-les aussi?
  • comment traiter les propriétés propres non énumérables?
  • qu'en est-il des nouveaux sous-arbres? Attribuez-vous simplement par référence ou par clone profond?
  • comment traiter les objets gelés/scellés/non extensibles?

Une autre chose à garder à l’esprit: les graphiques d’objets contenant des cycles. Ce n’est généralement pas difficile à gérer - conservez simplement une Set d’objets source déjà visités - mais souvent oubliée.

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 - en tant que sources de fusion. Jetez-la si elle rencontre un problème qu’elle 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 rouler le vôtre, soit rechercher une méthode de bibliothèque qui couvre vos cas d'utilisation.

91
the8472

Vous pouvez utiliser 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 }] }
85
AndrewHenderson

Voici une version immuable (ne modifie pas les entrées) de la réponse de @ Salakar. Utile si vous faites des choses 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;
}
52
CpILL

Puisque 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);
28
jhildenbiddle

Je sais qu'il y a déjà beaucoup de réponses et autant de commentaires prétendant qu'elles ne fonctionneront pas. Le seul consensus est que , c'est tellement compliqué que personne n'a fait de standard pour cela . Cependant, la plupart des réponses acceptées dans SO exposent des "astuces simples" 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 apprenant un peu plus sur la complexité de javascript, je vais essayer de vous éclairer un peu.

Avant de nous salir les mains, laissez-moi clarifier deux points:

  • [DISCLAIMER] Je propose ci-dessous une fonction qui traite de la façon dont nous boucle profonde en objets javascript pour la copie et illustre ce qui est généralement commenté trop rapidement. Ce n'est pas prêt pour la production. Par souci de clarté, j'ai volontairement laissé de côté d'autres considérations telles que objets circulaires (suivi par une propriété de symbole définie ou non contradictoire) , copie de la valeur de référence ou clone profond , objet de destination immuable (deep clone again?), étude au cas par cas de chaque type d’objets , obtenir/définir les propriétés via accessors ... En outre, 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 copier ou attribuer des termes au lieu de fusionner . Parce que dans mon esprit, une fusion est conservatrice et devrait échouer en cas de conflit. Ici, en cas de conflit, nous voulons que la source écrase la destination. Comme Object.assign le fait.

Les réponses avec for..in ou Object.keys sont trompeuses

Faire une copie en profondeur semble une pratique si élémentaire et si courante que nous nous attendons à trouver une solution unique ou, du moins, une victoire rapide via une simple récursion. Nous ne pensons pas avoir besoin d'une bibliothèque ou écrire une fonction personnalisée de 100 lignes.

Quand j'ai lu pour la première fois réponse de Salakar , j'ai vraiment pensé que je pouvais faire mieux et plus simplement (vous pouvez le comparer avec Object.assign sur x={a:1}, y={a:{b:1}}). Ensuite, j'ai lu réponse de the8472 et je me suis dit ... on ne peut pas s'éloigner si facilement, l'amélioration des réponses déjà données ne nous mènera pas loin.

Laissons les copies profondes et récursives de côté un instant. Il suffit d’examiner comment les personnes analysent (à tort) 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.keys omettra ses propres propriétés non énumérables, ses propres propriétés associées à des symboles et toutes les propriétés du prototype. Cela peut aller si vos objets n'en ont pas. Mais gardez à l'esprit que Object.assign gère ses propres propriétés énumérables à symbole. Donc, votre copie personnalisée a perdu sa floraison.

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

Si vous écrivez une fonction d'usage général et que vous n'utilisez pas Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames, Object.getOwnPropertySymbols ou Object.getPrototypeOf, vous le faites probablement mal.

Points à considérer avant d'écrire votre fonction

Tout d'abord, assurez-vous de bien comprendre ce qu'est un objet Javascript. En Javascript, un objet est constitué 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éfinissant une chaîne de prototypes.

Une propriété est une paire de clés (string ou symbol) et d'un descripteur (value ou get/set accesseur et d'attributs tels que enumerable ).

Enfin, il y a plusieurs types d'objets . Vous souhaiterez peut-être gérer différemment un objet Objet à partir d'un objet Date ou une fonction objet.

Donc, en écrivant votre copie conforme, vous devriez au moins répondre aux questions suivantes:

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

Pour mon exemple, j'estime que seuls les object Objects sont profond , car d'autres objets créés par d'autres constructeurs risquent de ne pas convenir à un examen approfondi. Personnalisé à partir de this 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 créé un objet options pour choisir le contenu à 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 } }
22
RaphaMex

J'utilise lodash:

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

Le paquet deepmerge npm semble être la bibliothèque la plus utilisée pour résoudre ce problème: https://www.npmjs.com/package/deepmerge

8
user3336882

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

Je voudrais présenter une alternative assez simple ES5. La fonction obtient 2 paramètres - target et source qui doivent être de type "objet". Target sera l'objet résultant. Target conserve toutes ses propriétés d'origine mais leurs valeurs peuvent ê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 target n'a pas de propriété source, target l'obtient;
  • si target a une propriété source et target & source ne sont pas les deux objets (3 cas sur 4), la propriété de target est annulée;
  • si target possède une propriété source et que tous deux sont des objets/des tableaux (il reste 1 cas), la récursivité se produit alors fusionnant deux objets (ou la concaténation de deux tableaux);

considérons également les éléments suivants :

  1. tableau + obj = tableau
  2. obj + array = obj
  3. obj + obj = obj (fusionné de manière récursive)
  4. tableau + tableau = tableau (concat)

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

jetez un coup d'œil à un exemple (et faites-le jouer si vous le souhaitez) :

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 émettront une erreur à un niveau de récursion très profond (pensez à des milliers d'appels imbriqués). De plus, vous êtes libre de traiter les situations telles que tableau + objet, etc., en ajoutant de nouvelles conditions et des contrôles de type.

7
curveball

Si vous utilisez ImmutableJS , vous pouvez utiliser mergeDeep:

fromJS(options).mergeDeep(options2).toJS();
7
Dimitri Kopriwa

Nous pouvons utiliser $ .Extend (true, object1, object2) pour une fusion profonde. La valeur true indique de fusionner deux objets de manière récursive en modifiant le premier.

$ extend (true, target, object)

5
Abinaya

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
}
5
pravdomil

La fonction suivante crée une copie profonde d’objets, elle couvre la copie primitive, les tableaux ainsi que les 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;
}
5
sudharsan tk

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

Si les bibliothèques npm peuvent être utilisées comme solution, object-merge-advanced de votre part permet vraiment de fusionner des objets en profondeur et de les personnaliser/substitue chaque action de fusion en utilisant une fonction de rappel familière. L'idée principale est plus qu'une simple fusion en profondeur - qu'advient-il de la valeur lorsque deux clés sont identiques ? Cette bibliothèque s’occupe de cela - lorsque deux clés s’entrechoquent, object-merge-advanced pèse les types, dans le but de conserver autant de données que possible après la fusion:

object key merging weighing key value types to retain as much data as possible

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

Lorsque les touches ne sont pas en conflit, elles entrent toutes dans le résultat.

À partir de votre exemple d’extrait de code, si vous avez utilisé object-merge-advanced pour fusionner 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 parcourt de manière récursive toutes les clés d'objet d'entrée, compare et construit et renvoie le nouveau résultat fusionné.

4
revelt

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));
4
y.c

J'avais ce problème lors du chargement d'un état de redux en cache. Si je ne fais que charger l'état 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 fonction merge que j'ai utilisée:

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

La plupart des exemples ici semblent trop complexes. J'en utilise un dans TypeScript que j'ai créé. Je pense que cela devrait couvrir la plupart des cas (je traite les tableaux comme des données ordinaires, en les remplaçant).

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

Faites-moi savoir s'il vous manque des fonctionnalités.

3
Ezequiel

Voici un autre que je viens d'écrire qui supporte les tableaux. Cela les concats.

function isObject(obj) {
    return obj !== null && typeof obj === 'object';
}


function isPlainObject(obj) {
    return isObject(obj) && (
        obj.constructor === Object  // obj = {}
        || obj.constructor === undefined // obj = Object.create(null)
    );
}

function mergeDeep(target, ...sources) {
    if (!sources.length) return target;
    const source = sources.shift();

    if(Array.isArray(target)) {
        if(Array.isArray(source)) {
            target.Push(...source);
        } else {
            target.Push(source);
        }
    } else if(isPlainObject(target)) {
        if(isPlainObject(source)) {
            for(let key of Object.keys(source)) {
                if(!target[key]) {
                    target[key] = source[key];
                } else {
                    mergeDeep(target[key], source[key]);
                }
            }
        } else {
            throw new Error(`Cannot merge object with non-object`);
        }
    } else {
        target = source;
    }

    return mergeDeep(target, ...sources);
};
2
mpen

Ramda qui est une belle bibliothèque de fonctions javascript a mergeDeepLeft et mergeDeepRight. N'importe lequel de ces 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}}
2
afonte

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

Si vous voulez avoir une seule ligne sans nécessiter une énorme 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.

1
modiX
// 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,
        },
      });
    });
  });
1
Sergey Gurin

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 une fonction (pas un constructeur).

1
Ruslan

Est-ce que quelqu'un sait s'il existe une fusion profonde dans la spécification ES6/ES7?

documentation Object.assign suggère qu'il ne fait pas de clonage en profondeur.

0
Gaurang Patel
function isObject(obj) {
    return obj !== null && typeof obj === 'object';
}
const isArray = Array.isArray;

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 (isPlainObject(source) || isArray(source)) {
        for (const key in source) {
            if (isPlainObject(source[key]) || isArray(source[key])) {
                if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
                    target[key] = {};
                }else if (isArray(source[key]) && !isArray(target[key])) {
                    target[key] = [];
                }
                mergeDeep(target[key], source[key]);
            } else if (source[key] !== undefined && source[key] !== '') {
                target[key] = source[key];
            }
        }
    }

    return mergeDeep(target, ...sources);
}

// test...
var source = {b:333};
var source2 = {c:32, arr: [33,11]}
var n = mergeDeep({a:33}, source, source2);
source2.arr[1] = 22;
console.log(n.arr); // out: [33, 11]
0
asins

Il existe déjà des bibliothèques bien gérées. Un exemple sur le registre npm est merge-deep

0
AliAvci

Il s'agit d'une fusion en profondeur peu coûteuse 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 }));
0
Lewis