Comment faire une comparaison approfondie entre 2 objets avec lodash?


309

J'ai 2 objets imbriqués qui sont différents et j'ai besoin de savoir s'ils ont des différences dans l'une de leurs propriétés imbriquées.

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

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

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

L'objet pourrait être beaucoup plus complexe avec plus de propriétés imbriquées. Mais celui-ci est un bon exemple. J'ai la possibilité d'utiliser des fonctions récursives ou quelque chose avec lodash ...


Pour une comparaison approfondie stackoverflow.com/a/46003894/696535
Pawel

7
_.isEqual(value, other)Effectue une comparaison approfondie entre deux valeurs pour déterminer si elles sont équivalentes. lodash.com/docs#isEqual
Lukas Liesis

JSON.stringify ()
xgqfrms

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

Réponses:


475

Une solution simple et élégante est à utiliser _.isEqual, qui effectue une comparaison approfondie:

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

Cependant, cette solution ne montre pas quelle propriété est différente.

http://jsfiddle.net/bdkeyn0h/


2
Je sais que la réponse est assez ancienne, mais je tiens à ajouter que cela _.isEqualpeut être assez délicat. Si vous copiez l'objet et modifiez certaines valeurs à l'intérieur, il restera vrai, car la référence est la même. Il faut donc être prudent en utilisant cette fonction.
oruckdeschel

23
@oruckdeschel si la référence est la même, c'est le même objet. il est donc égal. c'est un pointeur qui est délicat et non pas lodash. lodash est génial.
guy mograbi

265

Si vous avez besoin de savoir quelles propriétés sont différentes, utilisez réduire () :

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

36
Notez que cela ne produira que les différentes propriétés de premier niveau. (Donc, ce n'est pas vraiment profond dans le sens de produire des propriétés différentes.)
Bloke

16
De plus, cela ne ramassera pas les propriétés dans b qui ne sont pas dans a.
Ed Staub

3
et _.reduce(a, (result, value, key) => _.isEqual(value, b[key]) ? result : result.concat(key), [])pour une solution ES6 à une ligne
Dotgreg

1
Une version concédant la clé: valeurlet edited = _.reduce(a, function(result, value, key) { return _.isEqual(value, b[key]) ? result : result.concat( { [key]: value } ); }, []);
Aline Matos

47

Pour toute personne tombant sur ce fil, voici une solution plus complète. Il comparera deux objets et vous donnera la clé de toutes les propriétés qui sont soit uniquement dans object1 , uniquement dans object2 , soit dans object1 et object2 mais qui ont des valeurs différentes :

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

Voici un exemple de sortie:

// 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 vous ne vous souciez pas des objets imbriqués et que vous souhaitez ignorer lodash, vous pouvez remplacer le par _.isEqualpour une comparaison de valeurs normales, par exemple obj1[key] === obj2[key].


Cette réponse choisie est correcte pour simplement tester l'égalité. Si vous avez besoin de connaître les différences, il n'y a aucun moyen évident de les répertorier, mais cette réponse est plutôt bonne, donnant simplement une liste de clés de propriété de niveau supérieur où il y a une différence. (Et il donne la réponse en tant que fonction, ce qui le rend utilisable.)
Sigfried

Quelle est la différence entre faire cela et utiliser simplement _.isEqual (obj1, obj2)? Qu'est-ce que l'ajout de la vérification de hasOwnProperty fait que _.isEqual ne fait pas? J'étais sous l'hypothèse que si obj1 avait une propriété que obj2 n'avait pas, _.isEqual ne retournerait pas vrai ..?
Jaked222

2
@ Jaked222 - la différence est que isEqual renvoie un booléen vous indiquant si les objets sont égaux ou non, tandis que la fonction ci-dessus vous indique ce qui est différent entre les deux objets (s'ils sont différents). Si vous souhaitez seulement savoir si deux objets sont identiques, isEqual est tout à fait suffisant. Dans de nombreux cas, cependant, vous voulez savoir quelle est la différence entre deux objets. Un exemple pourrait être si vous souhaitez détecter les modifications avant et après quelque chose, puis distribuer un événement en fonction des modifications.
Johan Persson

30

Sur la base de la réponse d'Adam Boduch , j'ai écrit cette fonction qui compare deux objets dans le sens le plus profond possible , renvoyant des chemins qui ont des valeurs différentes ainsi que des chemins manquants dans l'un ou l'autre objet.

Le code n'a pas été écrit dans un souci d'efficacité et les améliorations à cet égard sont les bienvenues, mais voici la forme de 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;
}

Vous pouvez essayer le code à l'aide de cet extrait (il est recommandé de l'exécuter en mode pleine page):


4
Je viens de corriger le bogue, mais pour vous le faire savoir, vous devriez vérifier l'existence des clés dans un objet en b utilisant b.hasOwnProperty(key)oukey in b pas avec b[key] != undefined. Avec l'ancienne version utilisée b[key] != undefined, la fonction a renvoyé un diff incorrect pour les objets contenant undefined, comme dans compare({disabled: undefined}, {disabled: undefined}). En fait, l'ancienne version avait également des problèmes avec null; vous pouvez éviter de tels problèmes en utilisant toujours ===et!== au lieu de ==et !=.
Rory O'Kane

23

Voici une solution concise:

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

7
Ne semble pas fonctionner avec des objets pour moi. Renvoie à la place un tableau vide.
tomhughes

2
Obtention également d'un tableau vide avec Lodash 4.17.4
aristidesfl

@ Z.Khullah Si cela a fonctionné de cette façon, cela n'est pas documenté.
Brendon

1
@Brendon, @THughes, @aristidesfl désolé, j'ai mélangé des choses, cela fonctionne avec des tableaux d'objets, mais pas pour des comparaisons d'objets approfondies. Il s'avère que si aucun des paramètres n'est un tableau, lodash reviendra simplement [].
Z. Khullah

7

Pour montrer récursivement comment un objet est différent des autres, vous pouvez utiliser _.reduce combiné avec _.isEqual et _.isPlainObject . Dans ce cas, vous pouvez comparer en quoi a est différent de b ou en quoi b est différent de 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

_.isEqualMéthode d' utilisation simple, elle fonctionnera pour tous en comparant ...

  • Remarque: Cette méthode prend en charge la comparaison des tableaux, des tampons de tableau, des booléens, * des objets de date, des objets d'erreur, des cartes, des nombres, des Objectobjets, des expressions régulières, * des ensembles, des chaînes, des symboles et des tableaux typés. Objectles objets sont comparés * par leurs propres propriétés énumérables, non héritées. Les fonctions et les nœuds DOM * ne sont pas pris en charge.

Donc, si vous avez ci-dessous:

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

Si vous le faites: _.isEqual(firstName, otherName);,

ça reviendra vrai

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

Si vous le faites: _.isEqual(firstName, fullName);,

retournera faux


6

Ce code renvoie un objet avec toutes les propriétés qui ont une valeur différente ainsi que les valeurs des deux objets. Utile pour enregistrer la différence.

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

Sans utilisation de lodash / underscore, j'ai écrit ce code et fonctionne très bien pour moi pour une comparaison approfondie de object1 avec 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

Comparaison approfondie à l'aide d'un modèle de propriétés (imbriquées) pour vérifier

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)

Cela fonctionnera dans la console. La prise en charge des baies peut être ajoutée si nécessaire


2

J'ai essayé un code d'Adam Boduch pour produire un diff profond - ce n'est pas encore testé mais les morceaux sont 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
Fonctionne comme un charme, juste que l'ordre des obj1 et obj2 est important. Par exemple: diff({}, { foo: 'lol', bar: { baz: true }}) // returns []
amangpt777

2

Comme il a été demandé, voici une fonction de comparaison d'objets récursive. Et un peu plus. En supposant que l'utilisation principale d'une telle fonction est l'inspection d'objets, j'ai quelque chose à dire. Une comparaison approfondie complète est une mauvaise idée lorsque certaines différences ne sont pas pertinentes. Par exemple, une comparaison approfondie aveugle dans les assertions TDD rend les tests inutiles fragiles. Pour cette raison, je voudrais introduire un diff partiel beaucoup plus précieux . Il s'agit d'un analogue récursif d'une contribution précédente à ce fil. Il ignore les clés non présentes dans 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 permet de vérifier les valeurs attendues tout en tolérant d'autres propriétés, ce qui est exactement ce que vous souhaitez pour une inspection automatique . Cela permet de construire toutes sortes d'assertions avancées. Par exemple:

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

Revenons à la solution complète. Construire un diff traditionnel complet avec bdiff est 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));
};

L'exécution de la fonction ci-dessus sur deux objets complexes produira quelque chose de similaire à ceci:

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

Enfin, afin d'avoir un aperçu de la façon dont les valeurs diffèrent, nous pouvons vouloir évaluer directement () la sortie diff. Pour cela, nous avons besoin d'une version plus laide de bdiff qui génère des chemins syntaxiquement corrects:

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

Cela produira quelque chose de similaire à ceci:

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

Licence MIT;)


1

Complétant la réponse d'Adam Boduch, celui-ci prend en compte les différences de propriétés

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

Si vous n'avez besoin que d'une comparaison clé:

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

0

Voici un simple tapuscrit avec vérificateur de différence profonde Lodash qui produira un nouvel objet avec juste les différences entre un ancien objet et un nouvel objet.

Par exemple, si nous avions:

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

l'objet résultant serait:

const result: {b: 3};

Il est également compatible avec les objets profonds à plusieurs niveaux, pour les tableaux, il peut nécessiter quelques ajustements.

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

Ceci n'est pas valide. Vous ne pouvez pas comparer ===directement des objets avec , { a: 20 } === { a: 20 }retournera false, car il compare le prototype. La meilleure façon de comparer principalement des objets est de les envelopperJSON.stringify()
Herrgott

si (f === s) retourne vrai; - est uniquement pour la récursivité. Oui a: 20} === {a: 20} retournera faux et passera à la condition suivante
Crusader

pourquoi pas seulement _.isEqual(f, s)? :)
Herrgott

Cela se traduira par une boucle de récursivité infinie car si fest un objet et que vous y arrivez, if (_.isObject(f))revenez simplement sur la fonction et touchez à nouveau ce point. Il en va de mêmef (Array.isArray(f)&&Array.isArray(s))
Rady

-2

cela était basé sur @JLavoie , en utilisant 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

juste en utilisant vanille 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

entrez la description de l'image ici


1
Cette méthode ne vous dirait pas quels attributs sont différents.
JLavoie

2
Dans ce cas, les attributs influencent l'ordre dans le résultat.
Victor Oliveira

-2

Pour s'appuyer sur la réponse de Sridhar Gudimela , ici, elle est mise à jour d'une manière qui rendra Flow heureux:

"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;
};
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.