¿Cómo hacer una comparación profunda entre 2 objetos con lodash?

309

Tengo 2 objetos anidados que son diferentes y necesito saber si tienen una diferencia en una de sus propiedades anidadas.

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

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

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

El objeto podría ser mucho más complejo con más propiedades anidadas. Pero este es un buen ejemplo. Tengo la opción de usar funciones recursivas o algo con lodash ...

JLavoie
fuente
Para una comparación profunda stackoverflow.com/a/46003894/696535
Pawel
77
_.isEqual(value, other)Realiza una comparación profunda entre dos valores para determinar si son equivalentes. lodash.com/docs#isEqual
Lukas Liesis
JSON.stringify ()
xgqfrms
10
JSON.stringify () está mal: JSON.stringify ({a: 1, b: 2})! == JSON.stringify ({b: 2, a: 1})
Shl

Respuestas:

475

Es una solución fácil y elegante de usar _.isEqual, que realiza una comparación profunda:

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

Sin embargo, esta solución no muestra qué propiedad es diferente.

http://jsfiddle.net/bdkeyn0h/

JLavoie
fuente
2
Sé que la respuesta es bastante antigua, pero quiero agregar que eso _.isEqualpuede ser bastante complicado. Si copia el objeto y cambia algunos valores allí, seguirá siendo verdadero, porque la referencia es la misma. Así que uno debe tener cuidado al usar esta función.
oruckdeschel
23
@oruckdeschel si la referencia es la misma, es el mismo objeto. por eso es igual. Este es un puntero que es complicado, no lodash. Lodash es increíble.
Guy Mograbi
265

Si necesita saber qué propiedades son diferentes, use reduce () :

_.reduce(a, function(result, value, key) {
    return _.isEqual(value, b[key]) ?
        result : result.concat(key);
}, []);
// → [ "prop2" ]
Adam Boduch
fuente
36
Tenga en cuenta que esto solo generará las propiedades diferentes del primer nivel. (Por lo tanto, no es realmente profundo en el sentido de generar las propiedades que son diferentes.)
Bloke
16
Además, esto no recogerá propiedades en b que no estén en a.
Ed Staub el
3
y _.reduce(a, (result, value, key) => _.isEqual(value, b[key]) ? result : result.concat(key), [])para una solución ES6 de una línea
Dotgreg
1
Una versión que concatena la clave: valorlet edited = _.reduce(a, function(result, value, key) { return _.isEqual(value, b[key]) ? result : result.concat( { [key]: value } ); }, []);
Aline Matos
47

Para cualquiera que se encuentre con este hilo, aquí hay una solución más completa. Comparará dos objetos y le dará la clave de todas las propiedades que están solo en objeto1 , solo en objeto2 , o están en objeto1 y objeto2 pero tienen valores diferentes :

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

Aquí hay un ejemplo de salida:

// 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"]

Si no le importan los objetos anidados y desea omitir lodash, puede sustituirlo _.isEqualpor una comparación de valores normales, por ejemplo obj1[key] === obj2[key].

Johan Persson
fuente
Esta respuesta elegida es correcta solo para probar la igualdad. Si necesita saber cuáles son las diferencias, no hay una forma obvia de enumerarlas, pero esta respuesta es bastante buena, solo da una lista de claves de propiedades de nivel superior donde hay una diferencia. (Y da la respuesta como una función, lo que lo hace utilizable.)
Sigfried
¿Cuál es la diferencia entre hacer esto y usar _.isEqual (obj1, obj2)? ¿Qué hace la adición de la verificación de hasOwnProperty que _.isEqual no? Supuse que si obj1 tenía una propiedad que obj2 no tenía, _.isEqual no devolvería verdadero ...?
Jaked222
2
@ Jaked222: la diferencia es que isEqual devuelve un valor booleano que le indica si los objetos son iguales o no, mientras que la función anterior le dice qué es diferente entre los dos objetos (si son diferentes). Si solo le interesa saber si dos objetos son iguales, isEqual es suficiente. Sin embargo, en muchos casos, desea saber cuál es la diferencia entre dos objetos. Un ejemplo podría ser si desea detectar cambios antes y después de algo y luego enviar un evento basado en los cambios.
Johan Persson
30

Basado en la respuesta de Adam Boduch , escribí esta función que compara dos objetos en el sentido más profundo posible , devolviendo rutas que tienen valores diferentes, así como rutas que faltan en uno u otro objeto.

El código no fue escrito con la eficiencia en mente, y las mejoras en ese sentido son bienvenidas, pero aquí está la forma básica:

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

Puede probar el código con este fragmento (se recomienda ejecutar en modo de página completa):

Rashad Saleh
fuente
44
Acabo de corregir el error, pero para hacerle saber, debe verificar la existencia de claves dentro de un objeto b usando b.hasOwnProperty(key)okey in b , no con b[key] != undefined. Con la versión anterior que se usaba b[key] != undefined, la función devolvía un diff incorrecto para los objetos que contenían undefined, como en compare({disabled: undefined}, {disabled: undefined}). De hecho, la versión anterior también tenía problemas con null; puede evitar problemas como ese usando siempre ===y en!== lugar de ==y !=.
Rory O'Kane
23

Aquí hay una solución concisa:

_.differenceWith(a, b, _.isEqual);
DPG
fuente
77
No parece funcionar con objetos para mí. En su lugar, devuelve una matriz vacía.
tomhughes
2
También obteniendo una matriz vacía con Lodash 4.17.4
aristidesfl
@ Z.Khullah Si funcionó de esta manera, no está documentado.
Brendon
1
@Brendon, @THughes, @aristidesfl lo siento, he mezclado cosas, funciona con matrices de objetos, pero no para comparaciones de objetos profundos. Como resultado, si ninguno de los parámetros son matrices, lodash simplemente regresará [].
Z. Khullah
7

Para mostrar recursivamente en qué se diferencia un objeto con otro, puede usar _.reduce combinado con _.isEqual y _.isPlainObject . En este caso, puede comparar cómo a es diferente con b o cómo b es diferente con 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/[email protected]/lodash.min.js"></script>

Santiago Bendavid
fuente
7

_.isEqualMétodo de uso simple , funcionará para todas las comparaciones ...

  • Nota: Este método admite la comparación de matrices, memorias intermedias de matrices, booleanos, * objetos de fecha, objetos de error, mapas, números, Objectobjetos, expresiones regulares, * conjuntos, cadenas, símbolos y matrices escritas. Objectlos objetos se comparan * por sus propias propiedades enumerables, no heredadas. Las funciones y los nodos DOM * no son compatibles.

Entonces, si tienes a continuación:

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

Si lo hace: _.isEqual(firstName, otherName);,

volverá verdadero

Y si const fullName = {firstName: "Alireza", familyName: "Dezfoolian"};

Si lo hace: _.isEqual(firstName, fullName);,

devolverá falso

Alireza
fuente
6

Este código devuelve un objeto con todas las propiedades que tienen un valor diferente y también valores de ambos objetos. Útil para registrar la diferencia.

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;
}, {});
blikblum
fuente
3

Sin el uso de lodash / subrayado, he escrito este código y está funcionando bien para mí para una comparación profunda de object1 con 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;
}
Sridhar Gudimela
fuente
3

Comparación profunda utilizando una plantilla de propiedades (anidadas) para verificar

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)

Esto funcionará en la consola. Se podría agregar soporte de matriz si fuera necesario

Pawel
fuente
2

Tomé una puñalada del código de Adam Boduch para generar una diferencia profunda: esto no se ha probado por completo, pero las piezas están ahí:

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"]
Deminetix
fuente
1
Funciona como un encanto, solo que el orden de obj1 y obj2 es importante. Por ejemplo: diff({}, { foo: 'lol', bar: { baz: true }}) // returns []
amangpt777
2

Como se le preguntó, aquí hay una función de comparación recursiva de objetos. Y un poquito más. Suponiendo que el uso principal de dicha función es la inspección de objetos, tengo algo que decir. La comparación profunda completa es una mala idea cuando algunas diferencias son irrelevantes. Por ejemplo, la comparación ciega profunda en las afirmaciones TDD hace que las pruebas sean innecesariamente frágiles. Por esa razón, me gustaría presentar una diferencia parcial mucho más valiosa . Es un análogo recursivo de una contribución previa a este hilo. Ignora las claves no presentes en un

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 permite verificar los valores esperados mientras tolera otras propiedades, que es exactamente lo que desea para la inspección automática . Esto permite construir todo tipo de afirmaciones avanzadas. Por ejemplo:

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

Volviendo a la solución completa. Construir un diferencial tradicional completo con bdiff es trivial:

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

Ejecutar la función anterior en dos objetos complejos generará algo similar a esto:

 [
  " < 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"
 ]

Finalmente, para tener una idea de cómo difieren los valores, es posible que queramos evaluar directamente () la salida de diferencias. Para eso, necesitamos una versión más fea de bdiff que genere rutas sintácticamente correctas:

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

Eso generará algo similar a esto:

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

Licencia MIT;)

usuario46748
fuente
1

Completando la respuesta de Adam Boduch, esta toma en diferencias en las propiedades

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));
Ricardo Freitas
fuente
1

Si solo necesita una comparación de teclas:

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

Aquí hay un simple mecanografiado con el comprobador de diferencias profundas Lodash que producirá un nuevo objeto con solo las diferencias entre un objeto antiguo y uno nuevo.

Por ejemplo, si tuviéramos:

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

el objeto resultante sería:

const result: {b: 3};

También es compatible con objetos profundos de varios niveles, para matrices puede necesitar algunos ajustes.

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;
};
jmoore255
fuente
-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);
};
Cruzado
fuente
Esto no es valido. No puede comparar objetos ===directamente, { a: 20 } === { a: 20 }devolverá falso, porque compara el prototipo. Camino más derecho a comparar objetos principalmente es para envolverlos enJSON.stringify()
Herrgott
if (f === s) devuelve verdadero; - es solo para recursividad. Sí a: 20} === {a: 20} devolverá falso e irá a la siguiente condición
Crusader
¿Por qué no solo _.isEqual(f, s)? :)
Herrgott
Esto dará como resultado un bucle de recursión infinito porque si fes un objeto y llega a if (_.isObject(f))usted, simplemente regrese a través de la función y presione ese punto nuevamente. Lo mismo va paraf (Array.isArray(f)&&Array.isArray(s))
Rady
-2

esto se basó en @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/

Emiliano Barboza
fuente
-2

solo usando vanilla 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

ingrese la descripción de la imagen aquí

xgqfrms
fuente
1
Este método no le dirá qué atributos son diferentes.
JLavoie
2
En este caso, el orden de los atributos influye en el resultado.
Victor Oliveira
-2

Para construir sobre la respuesta de Sridhar Gudimela , aquí se actualiza de una manera que hará feliz a 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;
};
NetOperator Wibby
fuente