Come fare un confronto profondo tra 2 oggetti con lodash?


309

Ho 2 oggetti nidificati che sono diversi e devo sapere se hanno differenze in una delle loro proprietà nidificate.

var a = {};
var b = {};

a.prop1 = 2;
a.prop2 = { prop3: 2 };

b.prop1 = 2;
b.prop2 = { prop3: 3 };

L'oggetto potrebbe essere molto più complesso con proprietà più nidificate. Ma questo è un buon esempio. Ho la possibilità di usare funzioni ricorsive o qualcosa con lodash ...


Per un confronto approfondito stackoverflow.com/a/46003894/696535
Pawel

7
_.isEqual(value, other)Esegue un confronto approfondito tra due valori per determinare se sono equivalenti. lodash.com/docs#isEqual
Lukas Liesis

JSON.stringify ()
xgqfrms,

10
JSON.stringify () è errato: JSON.stringify ({a: 1, b: 2})! == JSON.stringify ({b: 2, a: 1})
Shl

Risposte:


475

Una soluzione semplice ed elegante da utilizzare _.isEqual, che esegue un confronto profondo:

var a = {};
var b = {};

a.prop1 = 2;
a.prop2 = { prop3: 2 };

b.prop1 = 2;
b.prop2 = { prop3: 3 };

_.isEqual(a, b); // returns false if different

Tuttavia, questa soluzione non mostra quale proprietà è diversa.

http://jsfiddle.net/bdkeyn0h/


2
So che la risposta è piuttosto vecchia, ma voglio aggiungere, che _.isEqualpuò essere piuttosto complicato. Se si copia l'oggetto e si modificano alcuni valori, verrà comunque visualizzato true, poiché il riferimento è lo stesso. Quindi si dovrebbe fare attenzione usando questa funzione.
oruckdeschel,

23
@oruckdeschel se il riferimento è lo stesso, è lo stesso oggetto. quindi è uguale. questo è un puntatore che è complicato, non lodash. lodash è fantastico.
guy mograbi,

265

Se devi sapere quali proprietà sono diverse, usa riduci () :

_.reduce(a, function(result, value, key) {
    return _.isEqual(value, b[key]) ?
        result : result.concat(key);
}, []);
// → [ "prop2" ]

36
Si noti che questo produrrà solo proprietà diverse di primo livello. (Quindi non è molto profondo nel senso di presentare le proprietà che sono diverse.)
Bloke

16
Inoltre, questo non raccoglierà le proprietà in b che non sono in a.
Ed Staub,

3
e _.reduce(a, (result, value, key) => _.isEqual(value, b[key]) ? result : result.concat(key), [])per una soluzione ES6 a una riga
Dotgreg

1
Una versione che concede la chiave: valorelet edited = _.reduce(a, function(result, value, key) { return _.isEqual(value, b[key]) ? result : result.concat( { [key]: value } ); }, []);
Aline Matos,

47

Per chiunque inciampare in questo thread, ecco una soluzione più completa. Confronterà due oggetti e ti fornirà la chiave di tutte le proprietà che sono solo nell'oggetto1 , solo nell'oggetto2 , oppure sono entrambe nell'oggetto1 e nell'oggetto2 ma hanno valori diversi :

/*
 * Compare two objects by reducing an array of keys in obj1, having the
 * keys in obj2 as the intial value of the result. Key points:
 *
 * - All keys of obj2 are initially in the result.
 *
 * - If the loop finds a key (from obj1, remember) not in obj2, it adds
 *   it to the result.
 *
 * - If the loop finds a key that are both in obj1 and obj2, it compares
 *   the value. If it's the same value, the key is removed from the result.
 */
function getObjectDiff(obj1, obj2) {
    const diff = Object.keys(obj1).reduce((result, key) => {
        if (!obj2.hasOwnProperty(key)) {
            result.push(key);
        } else if (_.isEqual(obj1[key], obj2[key])) {
            const resultKeyIndex = result.indexOf(key);
            result.splice(resultKeyIndex, 1);
        }
        return result;
    }, Object.keys(obj2));

    return diff;
}

Ecco un esempio di output:

// Test
let obj1 = {
    a: 1,
    b: 2,
    c: { foo: 1, bar: 2},
    d: { baz: 1, bat: 2 }
}

let obj2 = {
    b: 2, 
    c: { foo: 1, bar: 'monkey'}, 
    d: { baz: 1, bat: 2 }
    e: 1
}
getObjectDiff(obj1, obj2)
// ["c", "e", "a"]

Se non ti interessano gli oggetti nidificati e desideri saltare lodash, puoi sostituire il _.isEqualconfronto con un valore normale, ad es obj1[key] === obj2[key].


Questa risposta scelta è corretta solo per testare l'uguaglianza. Se hai bisogno di sapere quali sono le differenze, non esiste un modo ovvio per elencarle, ma questa risposta è piuttosto buona, fornendo solo un elenco di chiavi di proprietà di livello superiore in cui c'è una differenza. (E dà la risposta come una funzione, che la rende utilizzabile.)
Sigfried

Qual è la differenza tra fare questo e semplicemente usare _.isEqual (obj1, obj2)? Cosa fa l'aggiunta del controllo per hasOwnProperty che _.isEqual non fa? Pensavo che se obj1 avesse avuto una proprietà che obj2 non aveva, _.isEqual non sarebbe tornato vero ...?
Jaked222

2
@ Jaked222 - la differenza è che isEqual restituisce un valore booleano che ti dice se gli oggetti sono uguali o meno, mentre la funzione sopra ti dice cosa è diverso tra i due oggetti (se sono diversi). Se ti interessa solo sapere se due oggetti sono uguali, isEqual è abbastanza. In molti casi, però, vuoi sapere qual è la differenza tra due oggetti. Un esempio potrebbe essere se si desidera rilevare le modifiche prima e dopo qualcosa e quindi inviare un evento in base alle modifiche.
Johan Persson,

30

Sulla base della risposta di Adam Boduch , ho scritto questa funzione che confronta due oggetti nel senso più profondo possibile , restituendo percorsi con valori diversi e percorsi mancanti dall'uno o dall'altro oggetto.

Il codice non è stato scritto tenendo conto dell'efficienza e i miglioramenti in tal senso sono i benvenuti, ma ecco la forma di base:

var compare = function (a, b) {

  var result = {
    different: [],
    missing_from_first: [],
    missing_from_second: []
  };

  _.reduce(a, function (result, value, key) {
    if (b.hasOwnProperty(key)) {
      if (_.isEqual(value, b[key])) {
        return result;
      } else {
        if (typeof (a[key]) != typeof ({}) || typeof (b[key]) != typeof ({})) {
          //dead end.
          result.different.push(key);
          return result;
        } else {
          var deeper = compare(a[key], b[key]);
          result.different = result.different.concat(_.map(deeper.different, (sub_path) => {
            return key + "." + sub_path;
          }));

          result.missing_from_second = result.missing_from_second.concat(_.map(deeper.missing_from_second, (sub_path) => {
            return key + "." + sub_path;
          }));

          result.missing_from_first = result.missing_from_first.concat(_.map(deeper.missing_from_first, (sub_path) => {
            return key + "." + sub_path;
          }));
          return result;
        }
      }
    } else {
      result.missing_from_second.push(key);
      return result;
    }
  }, result);

  _.reduce(b, function (result, value, key) {
    if (a.hasOwnProperty(key)) {
      return result;
    } else {
      result.missing_from_first.push(key);
      return result;
    }
  }, result);

  return result;
}

Puoi provare il codice usando questo frammento (si consiglia l'esecuzione in modalità pagina intera):


4
Ho appena corretto il bug, ma per farti sapere, dovresti controllare l'esistenza della chiave all'interno di un oggetto b usando b.hasOwnProperty(key)okey in b , non con b[key] != undefined. Con la versione precedente utilizzata b[key] != undefined, la funzione ha restituito un diff errato per gli oggetti contenenti undefined, come in compare({disabled: undefined}, {disabled: undefined}). In effetti, anche la vecchia versione aveva problemi con null; puoi evitare problemi del genere usando sempre ===e!== invece di ==e !=.
Rory O'Kane,

23

Ecco una soluzione concisa:

_.differenceWith(a, b, _.isEqual);

7
Non sembra funzionare con gli oggetti per me. Restituisce invece un array vuoto.
tomhughes,

2
Anche ottenere array vuoti con Lodash 4.17.4
aristidesfl

@ Z.Khullah Se ha funzionato in questo modo, non è documentato.
Brendon,

1
@Brendon, @THughes, @aristidesfl scusate, ho mescolato le cose, funziona con array di oggetti, ma non per confronti di oggetti profondi. A quanto pare, se nessuno dei due parametri è una matrice, lodash ritornerà semplicemente [].
Z. Khullah,

7

Per mostrare in modo ricorsivo in che modo un oggetto è diverso dagli altri, puoi usare _.reduce combinato con _.isEqual e _.isPlainObject . In questo caso puoi confrontare come a è diversa da b o come b è diversa da a:

var a = {prop1: {prop1_1: 'text 1', prop1_2: 'text 2', prop1_3: [1, 2, 3]}, prop2: 2, prop3: 3};
var b = {prop1: {prop1_1: 'text 1', prop1_3: [1, 2]}, prop2: 2, prop3: 4};

var diff = function(obj1, obj2) {
  return _.reduce(obj1, function(result, value, key) {
    if (_.isPlainObject(value)) {
      result[key] = diff(value, obj2[key]);
    } else if (!_.isEqual(value, obj2[key])) {
      result[key] = value;
    }
    return result;
  }, {});
};

var res1 = diff(a, b);
var res2 = diff(b, a);
console.log(res1);
console.log(res2);
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.4/lodash.min.js"></script>


7

_.isEqualMetodo di utilizzo semplice , funzionerà per tutti i confronti ...

  • Nota: questo metodo supporta il confronto di matrici, buffer di array, valori booleani, * oggetti data, oggetti errore, mappe, numeri, Objectoggetti, regex, * set, stringhe, simboli e matrici tipizzate. Objectgli oggetti vengono confrontati * con le loro proprietà enumerabili, non ereditate. Le funzioni e i nodi DOM * non sono supportati.

Quindi se hai sotto:

 const firstName = {name: "Alireza"};
 const otherName = {name: "Alireza"};

Se lo fai: _.isEqual(firstName, otherName);,

tornerà vero

E se const fullName = {firstName: "Alireza", familyName: "Dezfoolian"};

Se lo fai: _.isEqual(firstName, fullName);,

restituirà false


6

Questo codice restituisce un oggetto con tutte le proprietà che hanno un valore diverso e anche i valori di entrambi gli oggetti. Utile per registrare la differenza.

var allkeys = _.union(_.keys(obj1), _.keys(obj2));
var difference = _.reduce(allkeys, function (result, key) {
  if ( !_.isEqual(obj1[key], obj2[key]) ) {
    result[key] = {obj1: obj1[key], obj2: obj2[key]}
  }
  return result;
}, {});

3

Senza usare lodash / underscore, ho scritto questo codice e sta funzionando bene per me per un confronto profondo tra object1 e object2

function getObjectDiff(a, b) {
    var diffObj = {};
    if (Array.isArray(a)) {
        a.forEach(function(elem, index) {
            if (!Array.isArray(diffObj)) {
                diffObj = [];
            }
            diffObj[index] = getObjectDiff(elem, (b || [])[index]);
        });
    } else if (a != null && typeof a == 'object') {
        Object.keys(a).forEach(function(key) {
            if (Array.isArray(a[key])) {
                var arr = getObjectDiff(a[key], b[key]);
                if (!Array.isArray(arr)) {
                    arr = [];
                }
                arr.forEach(function(elem, index) {
                    if (!Array.isArray(diffObj[key])) {
                        diffObj[key] = [];
                    }
                    diffObj[key][index] = elem;
                });
            } else if (typeof a[key] == 'object') {
                diffObj[key] = getObjectDiff(a[key], b[key]);
            } else if (a[key] != (b || {})[key]) {
                diffObj[key] = a[key];
            } else if (a[key] == (b || {})[key]) {
                delete a[key];
            }
        });
    }
    Object.keys(diffObj).forEach(function(key) {
        if (typeof diffObj[key] == 'object' && JSON.stringify(diffObj[key]) == '{}') {
            delete diffObj[key];
        }
    });
    return diffObj;
}

3

Confronto approfondito utilizzando un modello di proprietà (nidificate) da verificare

function objetcsDeepEqualByTemplate(objectA, objectB, comparisonTemplate) {
  if (!objectA || !objectB) return false

  let areDifferent = false
  Object.keys(comparisonTemplate).some((key) => {
    if (typeof comparisonTemplate[key] === 'object') {
      areDifferent = !objetcsDeepEqualByTemplate(objectA[key], objectB[key], comparisonTemplate[key])
      return areDifferent
    } else if (comparisonTemplate[key] === true) {
      areDifferent = objectA[key] !== objectB[key]
      return areDifferent
    } else {
      return false
    }
  })

  return !areDifferent
}

const objA = { 
  a: 1,
  b: {
    a: 21,
    b: 22,
  },
  c: 3,
}

const objB = { 
  a: 1,
  b: {
    a: 21,
    b: 25,
  },
  c: true,
}

// template tells which props to compare
const comparisonTemplateA = {
  a: true,
  b: {
    a: true
  }
}
objetcsDeepEqualByTemplate(objA, objB, comparisonTemplateA)
// returns true

const comparisonTemplateB = {
  a: true,
  c: true
}
// returns false
objetcsDeepEqualByTemplate(objA, objB, comparisonTemplateB)

Questo funzionerà nella console. Il supporto di array può essere aggiunto se necessario


2

Ho preso a pugni il codice di Adam Boduch per emettere un diff profondo - questo è del tutto non testato ma i pezzi sono lì:

function diff (obj1, obj2, path) {
    obj1 = obj1 || {};
    obj2 = obj2 || {};

    return _.reduce(obj1, function(result, value, key) {
        var p = path ? path + '.' + key : key;
        if (_.isObject(value)) {
            var d = diff(value, obj2[key], p);
            return d.length ? result.concat(d) : result;
        }
        return _.isEqual(value, obj2[key]) ? result : result.concat(p);
    }, []);
}

diff({ foo: 'lol', bar: { baz: true }}, {}) // returns ["foo", "bar.baz"]

1
Funziona come un incantesimo, solo che l'ordine di obj1 e obj2 è importante. Ad esempio: diff({}, { foo: 'lol', bar: { baz: true }}) // returns []
amangpt777

2

Come è stato chiesto, ecco una funzione di confronto di oggetti ricorsivi. E un po 'di più. Supponendo che l'uso primario di tale funzione sia l'ispezione degli oggetti, ho qualcosa da dire. Un confronto profondo completo è una cattiva idea quando alcune differenze sono irrilevanti. Ad esempio, un confronto profondo cieco nelle asserzioni TDD rende i test inutili e fragili. Per questo motivo, vorrei introdurre una diff parziale molto più preziosa . È un analogo ricorsivo di un precedente contributo a questo thread. Ignora le chiavi non presenti in a

var bdiff = (a, b) =>
    _.reduce(a, (res, val, key) =>
        res.concat((_.isPlainObject(val) || _.isArray(val)) && b
            ? bdiff(val, b[key]).map(x => key + '.' + x) 
            : (!b || val != b[key] ? [key] : [])),
        []);

BDiff consente di verificare i valori previsti tollerando altre proprietà, che è esattamente ciò che si desidera per l' ispezione automatica . Ciò consente di creare tutti i tipi di asserzioni avanzate. Per esempio:

var diff = bdiff(expected, actual);
// all expected properties match
console.assert(diff.length == 0, "Objects differ", diff, expected, actual);
// controlled inequality
console.assert(diff.length < 3, "Too many differences", diff, expected, actual);

Tornando alla soluzione completa. Costruire un diff tradizionale con bdiff è banale:

function diff(a, b) {
    var u = bdiff(a, b), v = bdiff(b, a);
    return u.filter(x=>!v.includes(x)).map(x=>' < ' + x)
    .concat(u.filter(x=>v.includes(x)).map(x=>' | ' + x))
    .concat(v.filter(x=>!u.includes(x)).map(x=>' > ' + x));
};

L'esecuzione sopra la funzione su due oggetti complessi genererà qualcosa di simile a questo:

 [
  " < components.0.components.1.components.1.isNew",
  " < components.0.cryptoKey",
  " | components.0.components.2.components.2.components.2.FFT.min",
  " | components.0.components.2.components.2.components.2.FFT.max",
  " > components.0.components.1.components.1.merkleTree",
  " > components.0.components.2.components.2.components.2.merkleTree",
  " > components.0.components.3.FFTResult"
 ]

Infine, per avere un'idea di come differiscono i valori, potremmo voler valutare direttamente () l'output diff. Per questo, abbiamo bisogno di una versione più brutta di bdiff che produca percorsi sintatticamente corretti:

// provides syntactically correct output
var bdiff = (a, b) =>
    _.reduce(a, (res, val, key) =>
        res.concat((_.isPlainObject(val) || _.isArray(val)) && b
            ? bdiff(val, b[key]).map(x => 
                key + (key.trim ? '':']') + (x.search(/^\d/)? '.':'[') + x)
            : (!b || val != b[key] ? [key + (key.trim ? '':']')] : [])),
        []);

// now we can eval output of the diff fuction that we left unchanged
diff(a, b).filter(x=>x[1] == '|').map(x=>[x].concat([a, b].map(y=>((z) =>eval('z.' + x.substr(3))).call(this, y)))));

Questo produrrà qualcosa di simile a questo:

[" | components[0].components[2].components[2].components[2].FFT.min", 0, 3]
[" | components[0].components[2].components[2].components[2].FFT.max", 100, 50]

Licenza MIT;)


1

Completando la risposta di Adam Boduch, questo prende in differenze di proprietà

const differenceOfKeys = (...objects) =>
  _.difference(...objects.map(obj => Object.keys(obj)));
const differenceObj = (a, b) => 
  _.reduce(a, (result, value, key) => (
    _.isEqual(value, b[key]) ? result : [...result, key]
  ), differenceOfKeys(b, a));

1

Se hai bisogno solo di un confronto chiave:

 _.reduce(a, function(result, value, key) {
     return b[key] === undefined ? key : []
  }, []);

0

Ecco un semplice dattiloscritto con il controllo delle differenze profonde di Lodash che produrrà un nuovo oggetto con solo le differenze tra un vecchio oggetto e un nuovo oggetto.

Ad esempio, se avessimo:

const oldData = {a: 1, b: 2};
const newData = {a: 1, b: 3};

l'oggetto risultante sarebbe:

const result: {b: 3};

È anche compatibile con oggetti profondi a più livelli, per le matrici potrebbe essere necessario apportare modifiche.

import * as _ from "lodash";

export const objectDeepDiff = (data: object | any, oldData: object | any) => {
  const record: any = {};
  Object.keys(data).forEach((key: string) => {
    // Checks that isn't an object and isn't equal
    if (!(typeof data[key] === "object" && _.isEqual(data[key], oldData[key]))) {
      record[key] = data[key];
    }
    // If is an object, and the object isn't equal
    if ((typeof data[key] === "object" && !_.isEqual(data[key], oldData[key]))) {
      record[key] = objectDeepDiff(data[key], oldData[key]);
    }
  });
  return record;
};

-1
var isEqual = function(f,s) {
  if (f === s) return true;

  if (Array.isArray(f)&&Array.isArray(s)) {
    return isEqual(f.sort(), s.sort());
  }
  if (_.isObject(f)) {
    return isEqual(f, s);
  }
  return _.isEqual(f, s);
};

Questo non è valido Non è possibile confrontare gli oggetti ===direttamente con , { a: 20 } === { a: 20 }restituirà false, perché confronta il prototipo. Il modo più giusto per confrontare principalmente gli oggetti è avvolgerli inJSON.stringify()
Herrgott il

se (f === s) restituisce vero; - è solo per la ricorsione. Sì a: 20} === {a: 20} restituirà false e passerà alla condizione successiva
Crusader,

perché non solo _.isEqual(f, s)? :)
Herrgott,

Ciò si tradurrà in un ciclo di ricorsione infinito perché se fè un oggetto e arrivi a if (_.isObject(f))te basta tornare indietro attraverso la funzione e premere di nuovo quel punto. Lo stesso vale perf (Array.isArray(f)&&Array.isArray(s))
Rady,

-2

questo era basato su @JLavoie , usando lodash

let differences = function (newObj, oldObj) {
      return _.reduce(newObj, function (result, value, key) {
        if (!_.isEqual(value, oldObj[key])) {
          if (_.isArray(value)) {
            result[key] = []
            _.forEach(value, function (innerObjFrom1, index) {
              if (_.isNil(oldObj[key][index])) {
                result[key].push(innerObjFrom1)
              } else {
                let changes = differences(innerObjFrom1, oldObj[key][index])
                if (!_.isEmpty(changes)) {
                  result[key].push(changes)
                }
              }
            })
          } else if (_.isObject(value)) {
            result[key] = differences(value, oldObj[key])
          } else {
            result[key] = value
          }
        }
        return result
      }, {})
    }

https://jsfiddle.net/EmilianoBarboza/0g0sn3b9/8/


-2

usando solo vaniglia js

let a = {};
let b = {};

a.prop1 = 2;
a.prop2 = { prop3: 2 };

b.prop1 = 2;
b.prop2 = { prop3: 3 };

JSON.stringify(a) === JSON.stringify(b);
// false
b.prop2 = { prop3: 2};

JSON.stringify(a) === JSON.stringify(b);
// true

inserisci qui la descrizione dell'immagine


1
Questo metodo non ti direbbe quali attributi sono diversi.
JLavoie,

2
In questo caso, gli attributi influenzano l'ordine nel risultato.
Victor Oliveira,

-2

Per basarsi sulla risposta di Sridhar Gudimela , qui viene aggiornato in modo da rendere felice Flow:

"use strict"; /* @flow */



//  E X P O R T

export const objectCompare = (objectA: any, objectB: any) => {
  let diffObj = {};

  switch(true) {
    case (Array.isArray(objectA)):
      objectA.forEach((elem, index) => {
        if (!Array.isArray(diffObj))
          diffObj = [];

        diffObj[index] = objectCompare(elem, (objectB || [])[index]);
      });

      break;

    case (objectA !== null && typeof objectA === "object"):
      Object.keys(objectA).forEach((key: any) => {
        if (Array.isArray(objectA[key])) {
          let arr = objectCompare(objectA[key], objectB[key]);

          if (!Array.isArray(arr))
            arr = [];

          arr.forEach((elem, index) => {
            if (!Array.isArray(diffObj[key]))
              diffObj[key] = [];

            diffObj[key][index] = elem;
          });
        } else if (typeof objectA[key] === "object")
          diffObj[key] = objectCompare(objectA[key], objectB[key]);
        else if (objectA[key] !== (objectB || {})[key])
          diffObj[key] = objectA[key];
        else if (objectA[key] === (objectB || {})[key])
          delete objectA[key];
      });

      break;

    default:
      break;
  }

  Object.keys(diffObj).forEach((key: any) => {
    if (typeof diffObj[key] === "object" && JSON.stringify(diffObj[key]) === "{}")
      delete diffObj[key];
  });

  return diffObj;
};
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.