Come unire in profondità invece di fondere in profondità?


340

Sia Object.assign che Object spread fanno solo un'unione superficiale.

Un esempio del problema:

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

L'output è quello che ti aspetteresti. Tuttavia, se provo questo:

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

Invece di

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

hai capito

{ a: { b: 1 } }

x viene completamente sovrascritto perché la sintassi di diffusione raggiunge solo un livello di profondità. Questo è lo stesso con Object.assign().

C'è un modo per fare questo?


la fusione profonda equivale a copiare le proprietà da un oggetto a un altro?

2
No, poiché le proprietà dell'oggetto non devono essere sovrascritte, ma ogni oggetto figlio deve essere unito allo stesso figlio sulla destinazione se esiste già.
Mike,

ES6 è finalizzato e nuove funzionalità non vengono più aggiunte, AFAIK.
Kangax,


1
@Oriol richiede jQuery però ...
m0meni

Risposte:


331

Qualcuno sa se esiste una fusione profonda nelle specifiche ES6 / ES7?

No non lo fa.


21
Si prega di rivedere la cronologia delle modifiche. Al momento in cui ho risposto a questa domanda, la domanda era : qualcuno sa se esiste una fusione profonda nelle specifiche ES6 / ES7? .

37
Questa risposta non si applica più a questa domanda - dovrebbe essere aggiornata o cancellata
DonVaughn,

13
La domanda non avrebbe dovuto essere modificata a questo livello. Le modifiche servono per chiarire. Una nuova domanda avrebbe dovuto essere pubblicata.
CJ Thompson,

171

So che questo è un po 'un vecchio problema, ma la soluzione più semplice in ES2015 / ES6 che ho potuto trovare era in realtà abbastanza semplice, usando Object.assign (),

Speriamo che questo aiuti:

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

Esempio di utilizzo:

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

Troverai una versione immutabile di questo nella risposta di seguito.

Si noti che ciò comporterà una ricorsione infinita su riferimenti circolari. Ci sono alcune grandi risposte qui su come rilevare riferimenti circolari se pensi di dover affrontare questo problema.


1
se il tuo grafico oggetto contiene cicli che porteranno a ricorsioni infinite
the8472

item !== nullnon dovrebbe essere necessario all'interno isObject, perché itemè già stata verificata la veridicità all'inizio della condizione
Mc

2
Perché scrivere questo: Object.assign(target, { [key]: {} })se potesse semplicemente essere target[key] = {}?
Jürg Lehni,

1
... e target[key] = source[key]invece diObject.assign(target, { [key]: source[key] });
Jürg Lehni,

3
Questo non supporta alcun oggetto non semplice in target. Ad esempio, mergeDeep({a: 3}, {a: {b: 4}})si tradurrà in un Numberoggetto aumentato , che chiaramente non è desiderato. Inoltre, isObjectnon accetta array, ma accetta qualsiasi altro tipo di oggetto nativo, ad esempio Date, che non deve essere copiato in profondità.
riv,

122

Puoi usare l'unione di 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 }] }

6
Ehi gente, questa è la soluzione più semplice e più bella. Lodash è fantastico, dovrebbero includerlo come core js object
Nurbol Alpysbayev

11
Il risultato non dovrebbe essere { 'a': [{ 'b': 2 }, { 'c': 3 }, { 'd': 4 }, { 'e': 5 }] }?
J. Hesters,

Buona domanda. Potrebbe essere una domanda separata o una per i manutentori di Lodash.
AndrewHenderson,

7
Il risultato { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }è corretto, perché stiamo unendo elementi di un array. L'elemento 0di object.aè {b: 2}, l'elemento 0di other.aè {c: 3}. Quando questi due vengono uniti perché hanno lo stesso indice di array, il risultato è { 'b': 2, 'c': 3 }, che è l'elemento 0nel nuovo oggetto.
Alexandru Furculita,

Preferisco questo , è 6 volte più piccolo con gzip.
Solo

101

Il problema non è banale quando si tratta di ospitare oggetti o qualsiasi tipo di oggetto che è più complesso di un sacco di valori

  • invochi un getter per ottenere un valore o copi sul descrittore di proprietà?
  • cosa succede se il target unione ha un setter (di proprietà o nella sua catena di prototipi)? Consideri il valore già presente o chiama il setter per aggiornare il valore corrente?
  • invochi funzioni di proprietà o le copi? Che cosa succede se sono funzioni associate o funzioni freccia che dipendono da qualcosa nella loro catena dell'ambito nel momento in cui sono state definite?
  • e se fosse qualcosa come un nodo DOM? Non vorrai certo trattarlo come un semplice oggetto e fondere in profondità tutte le sue proprietà
  • come gestire strutture "semplici" come matrici o mappe o set? Li consideri già presenti o li unisci anche tu?
  • come gestire le proprietà proprie non enumerabili?
  • che dire di nuovi sottoalberi? Assegnare semplicemente per riferimento o clone profondo?
  • come trattare oggetti congelati / sigillati / non estensibili?

Un'altra cosa da tenere a mente: i grafici degli oggetti che contengono cicli. Di solito non è difficile occuparsene - basta tenere un Setoggetto sorgente già visitato - ma spesso dimenticato.

Probabilmente dovresti scrivere una funzione di unione profonda che si aspetta solo valori primitivi e oggetti semplici - al massimo quei tipi che l' algoritmo di clone strutturato può gestire - come fonti di unione. Lancia se incontra qualcosa che non può gestire o semplicemente assegna per riferimento invece di una fusione profonda.

In altre parole, non esiste un algoritmo unico per tutti, devi rotolare il tuo o cercare un metodo di libreria che capiti di coprire i tuoi casi d'uso.


2
scuse per gli sviluppatori V8 di non implementare un trasferimento sicuro "stato del documento"
neaumusic

Sollevi molte buone questioni e mi sarebbe piaciuto vedere un'implementazione della tua raccomandazione. Quindi ho provato a crearne uno qui sotto. Potresti per favore dare un'occhiata e commentare? stackoverflow.com/a/48579540/8122487
RaphaMex

66

Ecco una versione immutabile (non modifica gli input) della risposta di @ Salakar. Utile se stai facendo cose di programmazione funzionale.

export function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item));
}

export default function mergeDeep(target, source) {
  let output = Object.assign({}, target);
  if (isObject(target) && isObject(source)) {
    Object.keys(source).forEach(key => {
      if (isObject(source[key])) {
        if (!(key in target))
          Object.assign(output, { [key]: source[key] });
        else
          output[key] = mergeDeep(target[key], source[key]);
      } else {
        Object.assign(output, { [key]: source[key] });
      }
    });
  }
  return output;
}

1
@torazaburo vedi il mio post precedente per la funzione
isObject

aggiornato. dopo alcuni test ho trovato un bug con gli oggetti profondamente nidificati
CpILL

3
È un nome di proprietà calcolato, il primo utilizzerà il valore di keycome nome della proprietà, il successivo renderà "chiave" il nome della proprietà. Vedi: es6-features.org/#ComputedPropertyNames
CpILL

2
in isObjectnon è necessario controllare && item !== nullalla fine, perché la linea inizia con item &&, no?
effimero

2
Se l'origine ha oggetti figlio nidificati più profondi del target, quegli oggetti faranno comunque riferimento agli stessi valori mergedDeepnell'output (credo). Ad esempio, 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 questo è un problema? Non muta gli ingressi, ma eventuali future mutazioni agli ingressi potrebbero mutare l'uscita, e viceversa con mutazioni per produrre ingressi mutanti. Per quello che vale, però, Ramda R.merge()ha lo stesso comportamento.
James Conkling,

40

Poiché questo problema è ancora attivo, ecco un altro approccio:

  • ES6 / 2015
  • Immutabile (non modifica gli oggetti originali)
  • Gestisce le matrici (le concatena)

/**
* Performs a deep merge of objects and returns new object. Does not modify
* objects (immutable) and merges arrays via concatenation.
*
* @param {...object} objects - Objects to merge
* @returns {object} New object with merged key/values
*/
function mergeDeep(...objects) {
  const isObject = obj => obj && typeof obj === 'object';
  
  return objects.reduce((prev, obj) => {
    Object.keys(obj).forEach(key => {
      const pVal = prev[key];
      const oVal = obj[key];
      
      if (Array.isArray(pVal) && Array.isArray(oVal)) {
        prev[key] = pVal.concat(...oVal);
      }
      else if (isObject(pVal) && isObject(oVal)) {
        prev[key] = mergeDeep(pVal, oVal);
      }
      else {
        prev[key] = oVal;
      }
    });
    
    return prev;
  }, {});
}

// Test objects
const obj1 = {
  a: 1,
  b: 1, 
  c: { x: 1, y: 1 },
  d: [ 1, 1 ]
}
const obj2 = {
  b: 2, 
  c: { y: 2, z: 2 },
  d: [ 2, 2 ],
  e: 2
}
const obj3 = mergeDeep(obj1, obj2);

// Out
console.log(obj3);


Questo è carino. Tuttavia, quando abbiamo array con elementi ripetuti, questi sono concatenati (ci sono elementi ripetuti). Ho adattato questo per prendere un parametro (array univoco: vero / falso).
Astronauta

1
Per rendere uniche le matrici è possibile passare prev[key] = pVal.concat(...oVal);aprev[key] = [...pVal, ...oVal].filter((element, index, array) => array.indexOf(element) === index);
Richard Herries,

1
Così bello e pulito !! Sicuramente la migliore risposta qui!
538ROMEO

Glorioso. Questo dimostra anche che le matrici si fondono, che è quello che stavo cercando.
Tschallacka,

Sì, si dice che la soluzione @CplLL è immutabile, ma utilizza la mutabilità effettiva dell'oggetto all'interno della funzione mentre l'utilizzo reduce non lo fa.
Augustin Riedinger,

30

So che ci sono già molte risposte e quanti commenti sostengono che non funzioneranno. L'unico consenso è che è così complicato che nessuno ha fatto uno standard per questo . Tuttavia, la maggior parte delle risposte accettate in SO espone "trucchi semplici" che sono ampiamente utilizzati. Quindi, per tutti noi come me che non siamo esperti ma che vogliono scrivere un codice più sicuro cogliendo qualcosa in più sulla complessità di JavaScript, proverò a fare luce.

Prima di sporcarci le mani, vorrei chiarire 2 punti:

  • [DISCLAIMER] Di seguito propongo una funzione che affronta il modo in cui ci immergiamo in profondità negli oggetti javascript per la copia e illustra ciò che è generalmente troppo brevemente commentato. Non è pronto per la produzione. Per motivi di chiarezza, ho volutamente lasciato da parte altre considerazioni come oggetti circolari (traccia da un set o proprietà del simbolo in conflitto) , copia del valore di riferimento o del clone profondo , oggetto di destinazione immutabile (nuovo clone profondo?), Studio caso per caso di ogni tipo di oggetto , ottenere / impostare le proprietà tramite accessori ... Inoltre, non ho testato le prestazioni, anche se è importante, perché non è nemmeno il punto qui.
  • Userò copia o assegnerò termini invece di unire . Perché nella mia mente una fusione è conservativa e dovrebbe fallire in caso di conflitto. Qui, in caso di conflitto, vogliamo che la fonte sovrascriva la destinazione. Piace Object.assign.

Le risposte con for..ino Object.keyssono fuorvianti

Fare una copia profonda sembra una pratica così semplice e comune che ci aspettiamo di trovare un one-liner o, almeno, una rapida vittoria tramite una semplice ricorsione. Non ci aspettiamo di dover avere bisogno di una libreria o di scrivere una funzione personalizzata di 100 righe.

Quando ho letto la risposta di Salakar per la prima volta , ho davvero pensato di poter fare meglio e più semplice (puoi confrontarlo con Object.assignon x={a:1}, y={a:{b:1}}). Poi ho letto la risposta di the8472 e ho pensato ... non c'è modo di scappare così facilmente, migliorare le risposte già fornite non ci porterà lontano.

Lasciamo copia profonda e ricorsivo da parte un istante. Considera solo come (erroneamente) le persone analizzano le proprietà per copiare un oggetto molto semplice.

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.keysometterà le proprietà non enumerabili, le proprietà con chiave di simbolo e tutte le proprietà del prototipo. Potrebbe andare bene se i tuoi oggetti non ne hanno nessuno. Ma tieni presente che Object.assigngestisce le proprietà enumerabili con chiave dei simboli. Quindi la tua copia personalizzata ha perso la sua fioritura.

for..infornirà le proprietà della fonte, del suo prototipo e dell'intera catena di prototipi senza che tu lo voglia (o non lo sappia). Il tuo obiettivo potrebbe finire con troppe proprietà, mescolando proprietà prototipo e proprietà proprie.

Se si scrive una funzione di uso generale e non si sta usando Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames, Object.getOwnPropertySymbolso Object.getPrototypeOf, si sta probabilmente facendo male.

Cose da considerare prima di scrivere la tua funzione

Innanzitutto, assicurati di capire cos'è un oggetto Javascript. In Javascript, un oggetto è composto dalle sue proprietà e da un oggetto prototipo (genitore). L'oggetto prototipo a sua volta è costituito da proprie proprietà e un oggetto prototipo. E così via, definendo una catena di prototipi.

Una proprietà è una coppia di chiave ( stringo symbol) e descrittore ( valueo get/ setaccessorio) e attributi similienumerable ).

Infine, ci sono molti tipi di oggetti . È possibile che si desideri gestire in modo diverso un oggetto Oggetto da un oggetto Data o una funzione oggetto.

Quindi, scrivendo la tua copia profonda, dovresti rispondere almeno a quelle domande:

  1. Cosa considero profondo (adatto per la ricerca ricorsiva) o piatto?
  2. Quali proprietà voglio copiare? (enumerabile / non enumerabile, chiave di stringa / chiave di simbolo, proprietà proprie / proprietà del prototipo, valori / descrittori ...)

Per il mio esempio, ritengo che solo le object Objects siano profonde , poiché altri oggetti creati da altri costruttori potrebbero non essere appropriati per uno sguardo approfondito. Personalizzato da questo 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 ho creato un optionsoggetto per scegliere cosa copiare (a scopo dimostrativo).

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

Funzione proposta

Puoi provarlo in questo 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;
    }
}

Che può essere usato in questo modo:

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

13

Uso lodash:

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

2
Nota che l'unione modificherà l'oggetto, se vuoi qualcosa che non muta l'oggetto, allora _cloneDeep(value1).merge(value2)
gechi

3
@geckos Puoi fare _.merge ({}, value1, value2)
Spenhouet,

10

Ecco l'implementazione di 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 test unitari:

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

9

Ecco un'altra soluzione ES6, funziona con oggetti e matrici.

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

3
è testato e / o parte di una libreria, sembra carino ma mi piacerebbe assicurarsi che sia in qualche modo provato.


8

Vorrei presentare un'alternativa ES5 piuttosto semplice. La funzione ottiene 2 parametri - targete sourcequesto deve essere di tipo "oggetto". Targetsarà l'oggetto risultante. Targetmantiene tutte le sue proprietà originali ma i loro valori possono essere modificati però.

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

casi:

  • se targetnon ha una sourceproprietà, la targetottiene;
  • se targetha una sourceproprietà e target& sourcenon sono entrambi oggetti (3 casi su 4), targetla proprietà viene sostituita;
  • se targetha una sourceproprietà ed entrambi sono oggetti / matrici (1 caso rimanente), allora la ricorsione avviene fondendo due oggetti (o concatenando due matrici);

considerare anche quanto segue :

  1. array + obj = array
  2. obj + array = obj
  3. obj + obj = obj (ricorsivamente unito)
  4. array + array = array (concat)

È prevedibile, supporta tipi primitivi, nonché matrici e oggetti. Inoltre, poiché possiamo unire 2 oggetti, penso che possiamo unire più di 2 tramite la funzione di riduzione .

guarda un esempio (e giocaci se vuoi) :

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

C'è una limitazione: la lunghezza dello stack delle chiamate del browser. I browser moderni genereranno un errore a un livello molto profondo di ricorsione (pensate a migliaia di chiamate nidificate). Inoltre sei libero di trattare situazioni come array + oggetto ecc. Come desideri aggiungendo nuove condizioni e controlli del tipo.



7

C'è un modo per fare questo?

Se le librerie npm possono essere utilizzate come soluzione, object-merge-advanced dal tuo consente davvero di unire gli oggetti in profondità e personalizzare / sovrascrivere ogni singola azione di unione utilizzando una familiare funzione di callback. L'idea principale è qualcosa di più della semplice fusione: cosa succede con il valore quando due chiavi sono uguali ? Questa libreria se ne occupa - quando due tasti si scontrano, object-merge-advancedpesa i tipi, con l'obiettivo di conservare quanti più dati possibili dopo l'unione:

fusione di oggetti chiave pesando tipi di valori chiave per conservare quanti più dati possibili

La chiave del primo argomento di input è contrassegnata con # 1, il secondo argomento con - # 2. A seconda del tipo, ne viene scelto uno per il valore della chiave del risultato. Nel diagramma, "un oggetto" significa un oggetto semplice (non array ecc.).

Quando le chiavi non si scontrano, tutte inseriscono il risultato.

Dal tuo snippet di esempio, se hai usato object-merge-advancedper unire il tuo snippet di codice:

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

Il suo algoritmo attraversa ricorsivamente tutte le chiavi dell'oggetto di input, confronta e costruisce e restituisce il nuovo risultato unito.


6

La seguente funzione crea una copia profonda degli oggetti, copre la copia di primitive, matrici e oggetti

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

6

Una soluzione semplice con ES5 (sovrascrivi il valore esistente):

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


proprio quello di cui avevo bisogno - es6 stava causando problemi nella costruzione - questa alternativa es5 è la bomba
danday74

5

La maggior parte degli esempi qui sembrano troppo complessi, ne sto usando uno in TypeScript che ho creato, penso che dovrebbe coprire la maggior parte dei casi (sto gestendo array come dati regolari, semplicemente sostituendoli).

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

Stessa cosa in JS semplice, nel caso in cui:

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

Ecco i miei casi di test per mostrare come potresti usarlo

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

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

Per favore fatemi sapere se pensate che mi manchi qualche funzionalità.


5

Se vuoi avere un solo liner senza richiedere un'enorme libreria come lodash, ti consiglio di usare deepmerge . ( npm install deepmerge)

Quindi puoi farlo

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

ottenere

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

La cosa bella è che arriva subito con le digitazioni per TypeScript. Inoltre, consente di unire le matrici . Questa è una vera soluzione a tutto tondo.


4

Possiamo usare $ .extend (true, object1, object2) per la fusione profonda. Il valore true indica l'unione ricorsiva di due oggetti, modificando il primo.

$ Estendere (vero, obiettivo, oggetto)


9
Il richiedente non ha mai indicato di utilizzare jquery e sembra richiedere una soluzione javascript nativa.
giovedì

Questo è un modo molto semplice per farlo e funziona. Una soluzione praticabile che prenderei in considerazione se fossi io a porre questa domanda. :)
Kashiraja,

Questa è un'ottima risposta ma manca un collegamento al codice sorgente di jQuery. jQuery ha un sacco di persone che lavorano al progetto e hanno trascorso un po 'di tempo a far funzionare correttamente la copia profonda. Inoltre, il codice sorgente è abbastanza "semplice": github.com/jquery/jquery/blob/master/src/core.js#L125 "Semplice" è tra virgolette perché inizia a complicarsi quando si scava jQuery.isPlainObject(). Ciò espone la complessità di determinare se qualcosa sia o meno un oggetto semplice, che la maggior parte delle risposte qui manca a un colpo lungo. Indovina in che lingua è scritto jQuery?
CubicleSoft

4

Qui una soluzione semplice e diretta che funziona come Object.assignsolo deeep e funziona per un array, senza alcuna modifica

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

Esempio

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


3

Ho riscontrato questo problema durante il caricamento di uno stato redux memorizzato nella cache. Se avessi appena caricato lo stato memorizzato nella cache, avrei incontrato errori per la nuova versione dell'app con una struttura di stato aggiornata.

È stato già menzionato, che lodash offre la mergefunzione, che ho usato:

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

3

Molte risposte usano decine di righe di codice o richiedono l'aggiunta di una nuova libreria al progetto, ma se si utilizza la ricorsione, si tratta solo di 4 righe di codice.

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

Gestione degli array: la versione precedente sovrascrive i vecchi valori dell'array con nuovi. Se si desidera che mantenga i vecchi valori dell'array e si aggiungano quelli nuovi, è sufficiente aggiungere un else if (current[key] instanceof Array && updates[key] instanceof Array) current[key] = current[key].concat(updates[key])blocco sopra lo elsestatamento e il gioco è fatto.


1
Mi piace, ma ha bisogno di un semplice controllo indefinito per "corrente" altrimenti {foo: undefined} non si fonde. Basta aggiungere un if (corrente) prima del ciclo for.
Andreas Pardeike,

Grazie per il suggerimento
Vincent

2

Eccone un altro che ho appena scritto che supporta array. Li concede.

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

Usa questa funzione:

merge(target, source, mutable = false) {
        const newObj = typeof target == 'object' ? (mutable ? target : Object.assign({}, target)) : {};
        for (const prop in source) {
            if (target[prop] == null || typeof target[prop] === 'undefined') {
                newObj[prop] = source[prop];
            } else if (Array.isArray(target[prop])) {
                newObj[prop] = source[prop] || target[prop];
            } else if (target[prop] instanceof RegExp) {
                newObj[prop] = source[prop] || target[prop];
            } else {
                newObj[prop] = typeof source[prop] === 'object' ? this.merge(target[prop], source[prop]) : source[prop];
            }
        }
        return newObj;
    }

2

Ramda, che è una bella libreria di funzioni javascript, ha mergeDeepLeft e mergeDeepRight. Ognuno di questi funziona abbastanza bene per questo problema. Dai un'occhiata alla documentazione qui: https://ramdajs.com/docs/#mergeDeepLeft

Per l'esempio specifico in questione possiamo usare:

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

2
// copies all properties from source object to dest object recursively
export function recursivelyMoveProperties(source, dest) {
  for (const prop in source) {
    if (!source.hasOwnProperty(prop)) {
      continue;
    }

    if (source[prop] === null) {
      // property is null
      dest[prop] = source[prop];
      continue;
    }

    if (typeof source[prop] === 'object') {
      // if property is object let's dive into in
      if (Array.isArray(source[prop])) {
        dest[prop] = [];
      } else {
        if (!dest.hasOwnProperty(prop)
        || typeof dest[prop] !== 'object'
        || dest[prop] === null || Array.isArray(dest[prop])
        || !Object.keys(dest[prop]).length) {
          dest[prop] = {};
        }
      }
      recursivelyMoveProperties(source[prop], dest[prop]);
      continue;
    }

    // property is simple type: string, number, e.t.c
    dest[prop] = source[prop];
  }
  return dest;
}

Test unitario:

describe('recursivelyMoveProperties', () => {
    it('should copy properties correctly', () => {
      const source: any = {
        propS1: 'str1',
        propS2: 'str2',
        propN1: 1,
        propN2: 2,
        propA1: [1, 2, 3],
        propA2: [],
        propB1: true,
        propB2: false,
        propU1: null,
        propU2: null,
        propD1: undefined,
        propD2: undefined,
        propO1: {
          subS1: 'sub11',
          subS2: 'sub12',
          subN1: 11,
          subN2: 12,
          subA1: [11, 12, 13],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
        propO2: {
          subS1: 'sub21',
          subS2: 'sub22',
          subN1: 21,
          subN2: 22,
          subA1: [21, 22, 23],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      };
      let dest: any = {
        propS2: 'str2',
        propS3: 'str3',
        propN2: -2,
        propN3: 3,
        propA2: [2, 2],
        propA3: [3, 2, 1],
        propB2: true,
        propB3: false,
        propU2: 'not null',
        propU3: null,
        propD2: 'defined',
        propD3: undefined,
        propO2: {
          subS2: 'inv22',
          subS3: 'sub23',
          subN2: -22,
          subN3: 23,
          subA2: [5, 5, 5],
          subA3: [31, 32, 33],
          subB2: false,
          subB3: true,
          subU2: 'not null --- ',
          subU3: null,
          subD2: ' not undefined ----',
          subD3: undefined,
        },
        propO3: {
          subS1: 'sub31',
          subS2: 'sub32',
          subN1: 31,
          subN2: 32,
          subA1: [31, 32, 33],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      };
      dest = recursivelyMoveProperties(source, dest);

      expect(dest).toEqual({
        propS1: 'str1',
        propS2: 'str2',
        propS3: 'str3',
        propN1: 1,
        propN2: 2,
        propN3: 3,
        propA1: [1, 2, 3],
        propA2: [],
        propA3: [3, 2, 1],
        propB1: true,
        propB2: false,
        propB3: false,
        propU1: null,
        propU2: null,
        propU3: null,
        propD1: undefined,
        propD2: undefined,
        propD3: undefined,
        propO1: {
          subS1: 'sub11',
          subS2: 'sub12',
          subN1: 11,
          subN2: 12,
          subA1: [11, 12, 13],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
        propO2: {
          subS1: 'sub21',
          subS2: 'sub22',
          subS3: 'sub23',
          subN1: 21,
          subN2: 22,
          subN3: 23,
          subA1: [21, 22, 23],
          subA2: [],
          subA3: [31, 32, 33],
          subB1: false,
          subB2: true,
          subB3: true,
          subU1: null,
          subU2: null,
          subU3: null,
          subD1: undefined,
          subD2: undefined,
          subD3: undefined,
        },
        propO3: {
          subS1: 'sub31',
          subS2: 'sub32',
          subN1: 31,
          subN2: 32,
          subA1: [31, 32, 33],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      });
    });
  });

2

Ho trovato solo una soluzione a 2 righe per ottenere una fusione profonda in JavaScript. Fammi sapere come funziona per te.

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'oggetto Temp stamperà {a: {b: 'd', e: 'f', x: 'y'}}


1
Questo non fa vera fusione profonda. Fallirà merge({x:{y:{z:1}}}, {x:{y:{w:2}}}). Il non riuscirà anche ad aggiornare i valori esistenti in obj1 se anche obj2 li ha, ad esempio con merge({x:{y:1}}, {x:{y:2}}).
Oreilles,

1

A volte non hai bisogno di una fusione profonda, anche se la pensi così. Ad esempio, se si dispone di una configurazione predefinita con oggetti nidificati e si desidera estenderla in profondità con la propria configurazione, è possibile creare una classe per questo. Il concetto è molto semplice:

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

Puoi convertirlo in una funzione (non un costruttore).


1

Questa è un'unione profonda ed economica che usa il codice che mi viene in mente. Ogni sorgente sovrascrive la proprietà precedente quando esiste.

const { keys } = Object;

const isObject = a => typeof a === "object" && !Array.isArray(a);
const merge = (a, b) =>
  isObject(a) && isObject(b)
    ? deepMerge(a, b)
    : isObject(a) && !isObject(b)
    ? a
    : b;

const coalesceByKey = source => (acc, key) =>
  (acc[key] && source[key]
    ? (acc[key] = merge(acc[key], source[key]))
    : (acc[key] = source[key])) && acc;

/**
 * Merge all sources into the target
 * overwriting primitive values in the the accumulated target as we go (if they already exist)
 * @param {*} target
 * @param  {...any} sources
 */
const deepMerge = (target, ...sources) =>
  sources.reduce(
    (acc, source) => keys(source).reduce(coalesceByKey(source), acc),
    target
  );

console.log(deepMerge({ a: 1 }, { a: 2 }));
console.log(deepMerge({ a: 1 }, { a: { b: 2 } }));
console.log(deepMerge({ a: { b: 2 } }, { a: 1 }));

1

Sto usando la seguente funzione breve per la fusione profonda di oggetti.
Mi va benissimo.
L'autore spiega completamente come funziona qui.

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

};

Sebbene questo collegamento possa rispondere alla domanda, è meglio includere qui le parti essenziali della risposta e fornire il collegamento come riferimento. Le risposte di solo collegamento possono diventare non valide se la pagina collegata cambia. - Dalla recensione
Chris Camaratta,

Ciao @ChrisCamaratta. Non solo è la parte essenziale qui, è tutto qui - la funzione e come usarla. Quindi questo non è sicuramente un link solo una risposta. Questa è la funzione che ho usato per unire in profondità gli oggetti. Il link è solo se vuoi che gli autori spieghino come funziona. Penso che sarebbe un disservizio per la comunità provare a spiegare meglio il funzionamento dell'autore che insegna JavaScript. Grazie per il commento.
John Shearing,

Huh. O l'ho perso o il codice non è stato visualizzato nell'interfaccia del revisore quando l'ho esaminato. Sono d'accordo che questa è una risposta di qualità. Sembrerebbe che altri revisori abbiano superato la mia valutazione iniziale, quindi penso che tu stia bene. Ci scusiamo per la bandiera dell'ispirazione.
Chris Camaratta il

Grande! @ChrisCamaratta, grazie per avermi aiutato a capire cosa è successo.
John Shearing,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.