Différence profonde générique entre deux objets

222

J'ai deux objets: oldObjet newObj.

Les données dans ont oldObjété utilisées pour remplir un formulaire et newObjsont le résultat de la modification des données de l'utilisateur dans ce formulaire et de leur soumission.

Les deux objets sont profonds, ie. ils ont des propriétés qui sont des objets ou des tableaux d'objets, etc. - ils peuvent avoir une profondeur de n niveaux, donc l'algorithme diff doit être récursif.

Maintenant , je dois non seulement comprendre ce qui a changé (comme ajoutée / mise à jour / supprimé) de oldObjla newObj, mais aussi la meilleure façon de le représenter.

Jusqu'à présent, je pensais simplement construire une genericDeepDiffBetweenObjectsméthode qui retournerait un objet sur le formulaire, {add:{...},upd:{...},del:{...}}mais j'ai pensé: quelqu'un d'autre devait en avoir besoin auparavant.

Alors ... quelqu'un connaît-il une bibliothèque ou un morceau de code qui fera cela et peut-être a-t-il une meilleure façon de représenter la différence (d'une manière qui est encore sérialisable JSON)?

Mettre à jour:

J'ai pensé à une meilleure façon de représenter les données mises à jour, en utilisant la même structure d'objet que newObj, mais en transformant toutes les valeurs de propriété en objets sur le formulaire:

{type: '<update|create|delete>', data: <propertyValue>}

Donc , si newObj.prop1 = 'new value'et oldObj.prop1 = 'old value'il fixeraitreturnObj.prop1 = {type: 'update', data: 'new value'}

Mise à jour 2:

Cela devient vraiment poilu lorsque nous arrivons à des propriétés qui sont des tableaux, car le tableau [1,2,3]doit être compté comme égal à [2,3,1], ce qui est assez simple pour les tableaux de types basés sur des valeurs comme string, int & bool, mais devient vraiment difficile à gérer en ce qui concerne tableaux de types de référence comme des objets et des tableaux.

Exemples de tableaux qui doivent être trouvés égaux:

[1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]

Non seulement il est assez complexe de vérifier ce type d'égalité de valeur profonde, mais aussi de trouver un bon moyen de représenter les changements qui pourraient être.

Martin Jespersen
la source
2
@ a'r: Ce n'est pas un doublon de stackoverflow.com/questions/1200562/… - Je sais comment parcourir les objets, je recherche de l'art antérieur car ce n'est pas anodin et sa mise en œuvre prendra du temps réel, et je préfère utiliser une bibliothèque plutôt que de la créer à partir de zéro.
Martin Jespersen
1
Avez-vous vraiment besoin de diff d'objets, est-ce que newObj généré à partir du serveur sur le formulaire soumet une réponse? Parce que si vous n'avez pas de «mises à jour du serveur» d'un objet, vous pouvez simplifier votre problème en attachant des écouteurs d'événements appropriés et lors de l'interaction de l'utilisateur (changement d'objet), vous pouvez mettre à jour / générer la liste des modifications souhaitées.
sbgoran
1
@sbgoran: newObjest généré par le code js qui lit les valeurs d'un formulaire dans le DOM. Il y a plusieurs façons de garder l'état et de le faire beaucoup plus facilement, mais j'aimerais le garder apatride comme exercice. Je suis également à la recherche d'antériorités pour voir comment d'autres auraient pu résoudre ce problème, si quelqu'un l'a fait.
Martin Jespersen
3
voici une bibliothèque très sophistiquée pour diff / patcher n'importe quelle paire d'objets Javascript github.com/benjamine/jsondiffpatch vous pouvez le voir en direct ici: benjamine.github.io/jsondiffpatch/demo/index.html (avertissement: je suis l'auteur)
Benja

Réponses:

141

J'ai écrit un petit cours qui fait ce que tu veux, tu peux le tester ici .

La seule chose qui est différente de votre proposition est que je ne considère [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]pas la même chose, car je pense que les tableaux ne sont pas égaux si l'ordre de leurs éléments n'est pas le même. Bien sûr, cela peut être modifié si nécessaire. De plus, ce code peut être encore amélioré pour prendre la fonction comme argument qui sera utilisé pour formater un objet diff de manière arbitraire en fonction des valeurs primitives passées (maintenant ce travail est effectué par la méthode "compareValues").

var deepDiffMapper = function () {
  return {
    VALUE_CREATED: 'created',
    VALUE_UPDATED: 'updated',
    VALUE_DELETED: 'deleted',
    VALUE_UNCHANGED: 'unchanged',
    map: function(obj1, obj2) {
      if (this.isFunction(obj1) || this.isFunction(obj2)) {
        throw 'Invalid argument. Function given, object expected.';
      }
      if (this.isValue(obj1) || this.isValue(obj2)) {
        return {
          type: this.compareValues(obj1, obj2),
          data: obj1 === undefined ? obj2 : obj1
        };
      }

      var diff = {};
      for (var key in obj1) {
        if (this.isFunction(obj1[key])) {
          continue;
        }

        var value2 = undefined;
        if (obj2[key] !== undefined) {
          value2 = obj2[key];
        }

        diff[key] = this.map(obj1[key], value2);
      }
      for (var key in obj2) {
        if (this.isFunction(obj2[key]) || diff[key] !== undefined) {
          continue;
        }

        diff[key] = this.map(undefined, obj2[key]);
      }

      return diff;

    },
    compareValues: function (value1, value2) {
      if (value1 === value2) {
        return this.VALUE_UNCHANGED;
      }
      if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
        return this.VALUE_UNCHANGED;
      }
      if (value1 === undefined) {
        return this.VALUE_CREATED;
      }
      if (value2 === undefined) {
        return this.VALUE_DELETED;
      }
      return this.VALUE_UPDATED;
    },
    isFunction: function (x) {
      return Object.prototype.toString.call(x) === '[object Function]';
    },
    isArray: function (x) {
      return Object.prototype.toString.call(x) === '[object Array]';
    },
    isDate: function (x) {
      return Object.prototype.toString.call(x) === '[object Date]';
    },
    isObject: function (x) {
      return Object.prototype.toString.call(x) === '[object Object]';
    },
    isValue: function (x) {
      return !this.isObject(x) && !this.isArray(x);
    }
  }
}();


var result = deepDiffMapper.map({
  a: 'i am unchanged',
  b: 'i am deleted',
  e: {
    a: 1,
    b: false,
    c: null
  },
  f: [1, {
    a: 'same',
    b: [{
      a: 'same'
    }, {
      d: 'delete'
    }]
  }],
  g: new Date('2017.11.25')
}, {
  a: 'i am unchanged',
  c: 'i am created',
  e: {
    a: '1',
    b: '',
    d: 'created'
  },
  f: [{
    a: 'same',
    b: [{
      a: 'same'
    }, {
      c: 'create'
    }]
  }, 1],
  g: new Date('2017.11.25')
});
console.log(result);

sbgoran
la source
3
+1 Ce n'est pas un mauvais morceau de code. Il y a cependant un bogue (vérifiez cet exemple: jsfiddle.net/kySNu/3 c est créé comme undefinedmais devrait être la chaîne 'i am created'), et en plus il ne fait pas ce dont j'ai besoin car il manque la valeur du tableau profond comparer qui est le partie la plus cruciale (et complexe / difficile). En remarque, la construction 'array' != typeof(obj)est inutile car les tableaux sont des objets qui sont des instances de tableaux.
Martin Jespersen
1
J'ai mis à jour le code, mais je ne sais pas quelle valeur vous voulez dans l'objet résultant, en ce moment, le code renvoie la valeur du premier objet et s'il n'existe pas, la valeur du second sera définie comme données.
sbgoran
1
Et comment voulez-vous dire "manque de comparaison de la valeur du tableau profond" pour les tableaux que vous obtiendrez pour chaque index de cet {type: ..., data:..}objet. Ce qui manque, c'est la recherche de la valeur du premier tableau dans le second, mais comme je l'ai mentionné dans ma réponse, je ne pense pas que les tableaux soient égaux si l'ordre de leurs valeurs n'est pas égal ( [1, 2, 3] is not equal to [3, 2, 1]à mon avis).
sbgoran
6
@MartinJespersen OK, comment voulez - vous traiter génériquement ces tableaux alors: [{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]. Le premier objet du premier tableau est maintenant mis à jour avec "valeur1" ou "valeur2". Et ceci est un exemple simple, cela pourrait devenir beaucoup plus compliqué avec une imbrication profonde. Si vous voulez / besoin comparaison profonde imbrication indépendamment de la position clé ne créent pas des tableaux d'objets, créer des objets avec des objets imbriqués comme par exemple précédent: {inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}.
sbgoran
2
Je suis d'accord avec vous sur le dernier point de vue - la structure de données d'origine devrait être changée en quelque chose qui est plus facile à faire une différence réelle. Félicitations, vous avez réussi :)
Martin Jespersen
88

En utilisant Underscore, un simple diff:

var o1 = {a: 1, b: 2, c: 2},
    o2 = {a: 2, b: 1, c: 2};

_.omit(o1, function(v,k) { return o2[k] === v; })

Les résultats dans les parties de o1cela correspondent mais avec des valeurs différentes dans o2:

{a: 1, b: 2}

Ce serait différent pour une différence profonde:

function diff(a,b) {
    var r = {};
    _.each(a, function(v,k) {
        if(b[k] === v) return;
        // but what if it returns an empty object? still attach?
        r[k] = _.isObject(v)
                ? _.diff(v, b[k])
                : v
            ;
        });
    return r;
}

Comme souligné par @Juhana dans les commentaires, ce qui précède n'est qu'un diff a -> b et non réversible (ce qui signifie que les propriétés supplémentaires dans b seraient ignorées). Utilisez plutôt a -> b -> a:

(function(_) {
  function deepDiff(a, b, r) {
    _.each(a, function(v, k) {
      // already checked this or equal...
      if (r.hasOwnProperty(k) || b[k] === v) return;
      // but what if it returns an empty object? still attach?
      r[k] = _.isObject(v) ? _.diff(v, b[k]) : v;
    });
  }

  /* the function */
  _.mixin({
    diff: function(a, b) {
      var r = {};
      deepDiff(a, b, r);
      deepDiff(b, a, r);
      return r;
    }
  });
})(_.noConflict());

Voir http://jsfiddle.net/drzaus/9g5qoxwj/ pour un exemple complet + tests + mixins

drzaus
la source
Je ne sais pas pourquoi vous avez voté contre, cela était suffisant car vous avez fourni un exemple simple et peu profond ainsi qu'une fonction profonde plus complexe.
Seiyria
2
@Seiyria déteste va détester, je suppose ... J'ai fait les deux parce que je pensais à l'origine que ce omitserait une différence profonde, mais j'avais tort, donc inclus également à des fins de comparaison.
drzaus
1
Belle solution. Je suggère de changer r[k] = ... : vde r[k] = ... : {'a':v, 'b':b[k] }cette façon, vous pouvez voir deux valeurs.
guyaloni
2
Les deux retournent un faux négatif lorsque les objets sont par ailleurs identiques mais que le second a plus d'éléments, par exemple {a:1, b:2}et {a:1, b:2, c:3}.
JJJ
1
Ce devrait être _.omitByau lieu de _.omit.
JP
48

J'aimerais offrir une solution ES6 ... Il s'agit d'un différentiel à sens unique, ce qui signifie qu'il retournera des clés / valeurs o2qui ne sont pas identiques à leurs homologues dans o1:

let o1 = {
  one: 1,
  two: 2,
  three: 3
}

let o2 = {
  two: 2,
  three: 3,
  four: 4
}

let diff = Object.keys(o2).reduce((diff, key) => {
  if (o1[key] === o2[key]) return diff
  return {
    ...diff,
    [key]: o2[key]
  }
}, {})
senornestor
la source
3
Belle solution mais vous voudrez peut-être vérifier cette if(o1[key] === o1[key])ligne mec
bm_i
Le code est-il complet? Je reçoisUncaught SyntaxError: Unexpected token ...
Seano
2
J'aime la solution mais elle a un problème, si l'objet est plus profond qu'un niveau, il renverra toutes les valeurs dans les objets imbriqués modifiés - ou du moins c'est ce qui se passe pour moi.
Spurious
3
Ouaip, ce n'est pas récursif @Spurious
Nemesarial
2
Gardez à l'esprit qu'avec cette solution, pour chaque élément de l'objet, vous obtenez un objet entièrement nouveau construit avec tous les éléments existants copiés dedans juste pour ajouter un élément au tableau. Pour les petits objets, c'est bien, mais cela ralentira de façon exponentielle pour les objets plus gros.
Malvineous
22

Utilisation de Lodash:

_.mergeWith(oldObj, newObj, function (objectValue, sourceValue, key, object, source) {
    if ( !(_.isEqual(objectValue, sourceValue)) && (Object(objectValue) !== objectValue)) {
        console.log(key + "\n    Expected: " + sourceValue + "\n    Actual: " + objectValue);
    }
});

Je n'utilise pas de clé / objet / source mais je l'ai laissé là si vous avez besoin d'y accéder. La comparaison d'objets empêche simplement la console d'imprimer les différences sur la console de l'élément le plus externe à l'élément le plus interne.

Vous pouvez ajouter un peu de logique à l'intérieur pour gérer les tableaux. Peut-être triez les tableaux en premier. Il s'agit d'une solution très flexible.

ÉDITER

Modifié de _.merge à _.mergeWith en raison de la mise à jour de lodash. Merci à Aviron d'avoir remarqué le changement.

toshiomagic
la source
6
Dans lodash 4.15.0, _.merge avec la fonction de personnalisation n'est plus pris en charge, vous devez donc utiliser _.mergeWith à la place.
Aviran Cohen
1
cette fonction est excellente mais ne fonctionne pas dans un objet imbriqué.
Joe Allen
13

Voici une bibliothèque JavaScript que vous pouvez utiliser pour rechercher des différences entre deux objets JavaScript:

URL Github: https://github.com/cosmicanant/recursive-diff

URL Npmjs: https://www.npmjs.com/package/recursive-diff

Vous pouvez utiliser la bibliothèque récursive-diff dans le navigateur ainsi que Node.js. Pour le navigateur, procédez comme suit:

<script type="text" src="https://unpkg.com/[email protected]/dist/recursive-diff.min.js"/>
<script type="text/javascript">
     const ob1 = {a:1, b: [2,3]};
     const ob2 = {a:2, b: [3,3,1]};
     const delta = recursiveDiff.getDiff(ob1,ob2); 
     /* console.log(delta) will dump following data 
     [
         {path: ['a'], op: 'update', val: 2}
         {path: ['b', '0'], op: 'update',val: 3},
         {path: ['b',2], op: 'add', val: 1 },
     ]
      */
     const ob3 = recursiveDiff.applyDiff(ob1, delta); //expect ob3 is deep equal to ob2
 </script>

Alors que dans node.js, vous pouvez avoir besoin d'un module 'récursif-diff' et l'utiliser comme ci-dessous:

const diff = require('recursive-diff');
const ob1 = {a: 1}, ob2: {b:2};
const diff = diff.getDiff(ob1, ob2);
Une fourmi
la source
Cela ne tiendra pas compte des changements dans les propriétés de date, par exemple.
trollkotze
date à laquelle le support est ajouté
Anant
9

De nos jours, il y a pas mal de modules disponibles pour cela. J'ai récemment écrit un module pour ce faire, car je n'étais pas satisfait des nombreux modules différents que j'ai trouvés. Son appelé odiff: https://github.com/Tixit/odiff . J'ai également énuméré un tas des modules les plus populaires et pourquoi ils n'étaient pas acceptables dans le fichier Lisez- odiffmoi, que vous pouvez consulter si odiffvous n'avez pas les propriétés souhaitées. Voici un exemple:

var a = [{a:1,b:2,c:3},              {x:1,y: 2, z:3},              {w:9,q:8,r:7}]
var b = [{a:1,b:2,c:3},{t:4,y:5,u:6},{x:1,y:'3',z:3},{t:9,y:9,u:9},{w:9,q:8,r:7}]

var diffs = odiff(a,b)

/* diffs now contains:
[{type: 'add', path:[], index: 2, vals: [{t:9,y:9,u:9}]},
 {type: 'set', path:[1,'y'], val: '3'},
 {type: 'add', path:[], index: 1, vals: [{t:4,y:5,u:6}]}
]
*/
BT
la source
7
const diff = require("deep-object-diff").diff;
let differences = diff(obj2, obj1);

Il existe un module npm avec plus de 500 000 téléchargements hebdomadaires: https://www.npmjs.com/package/deep-object-diff

J'aime l'objet comme la représentation des différences - surtout il est facile de voir la structure, quand elle est formatée.

const diff = require("deep-object-diff").diff;

const lhs = {
  foo: {
    bar: {
      a: ['a', 'b'],
      b: 2,
      c: ['x', 'y'],
      e: 100 // deleted
    }
  },
  buzz: 'world'
};

const rhs = {
  foo: {
    bar: {
      a: ['a'], // index 1 ('b')  deleted
      b: 2, // unchanged
      c: ['x', 'y', 'z'], // 'z' added
      d: 'Hello, world!' // added
    }
  },
  buzz: 'fizz' // updated
};

console.log(diff(lhs, rhs)); // =>
/*
{
  foo: {
    bar: {
      a: {
        '1': undefined
      },
      c: {
        '2': 'z'
      },
      d: 'Hello, world!',
      e: undefined
    }
  },
  buzz: 'fizz'
}
*/
Felix Furtmayr
la source
2

J'ai utilisé ce morceau de code pour effectuer la tâche que vous décrivez:

function mergeRecursive(obj1, obj2) {
    for (var p in obj2) {
        try {
            if(obj2[p].constructor == Object) {
                obj1[p] = mergeRecursive(obj1[p], obj2[p]);
            }
            // Property in destination object set; update its value.
            else if (Ext.isArray(obj2[p])) {
                // obj1[p] = [];
                if (obj2[p].length < 1) {
                    obj1[p] = obj2[p];
                }
                else {
                    obj1[p] = mergeRecursive(obj1[p], obj2[p]);
                }

            }else{
                obj1[p] = obj2[p];
            }
        } catch (e) {
            // Property in destination object not set; create it and set its value.
            obj1[p] = obj2[p];
        }
    }
    return obj1;
}

cela vous donnera un nouvel objet qui fusionnera toutes les modifications entre l'ancien objet et le nouvel objet de votre formulaire

Un membre
la source
1
J'utilise le framework Ext ici, mais vous pouvez le remplacer et utiliser
n'importe
La fusion d'objets est triviale et peut être aussi simple que d' $.extend(true,obj1,obj2)utiliser jQuery. Ce n'est pas du tout ce dont j'ai besoin. J'ai besoin de la différence entre les deux objets et non de leur combinaison.
Martin Jespersen
son grand que Ext est utilisé ici
peroxyde
2

J'ai développé la fonction nommée "compareValue ()" en Javascript. il retourne si la valeur est identique ou non. J'ai appelé compareValue () dans la boucle for d'un objet. vous pouvez obtenir la différence de deux objets dans diffParams.

var diffParams = {};
var obj1 = {"a":"1", "b":"2", "c":[{"key":"3"}]},
    obj2 = {"a":"1", "b":"66", "c":[{"key":"55"}]};

for( var p in obj1 ){
  if ( !compareValue(obj1[p], obj2[p]) ){
    diffParams[p] = obj1[p];
  }
}

function compareValue(val1, val2){
  var isSame = true;
  for ( var p in val1 ) {

    if (typeof(val1[p]) === "object"){
      var objectValue1 = val1[p],
          objectValue2 = val2[p];
      for( var value in objectValue1 ){
        isSame = compareValue(objectValue1[value], objectValue2[value]);
        if( isSame === false ){
          return false;
        }
      }
    }else{
      if(val1 !== val2){
        isSame = false;
      }
    }
  }
  return isSame;
}
console.log(diffParams);

jarangseo
la source
1

Je sais que je suis en retard à la fête, mais j'avais besoin de quelque chose de similaire que les réponses ci-dessus n'ont pas aidé.

J'utilisais la fonction $ watch d'Angular pour détecter les changements dans une variable. Non seulement je devais savoir si une propriété avait changé sur la variable, mais je voulais également m'assurer que la propriété qui avait changé n'était pas un champ calculé temporaire. En d'autres termes, je voulais ignorer certaines propriétés.

Voici le code: https://jsfiddle.net/rv01x6jo/

Voici comment l'utiliser:

// To only return the difference
var difference = diff(newValue, oldValue);  

// To exclude certain properties
var difference = diff(newValue, oldValue, [newValue.prop1, newValue.prop2, newValue.prop3]);

J'espère que cela aide quelqu'un.

a11smiles
la source
Veuillez également inclure le code dans votre réponse, pas seulement un violon.
xpy
Il semble que defineProperty résoudrait ce problème avec de meilleures performances, si je me souviens bien, cela fonctionne jusqu'à IE9.
Peter
Merci..!! Votre code fonctionne comme un charme et m'a sauvé la journée. J'ai un objet json de 1250 lignes et cela me donne un o / p exact que je veux.
Tejas Mehta
1

J'utilise juste ramda, pour résoudre le même problème, j'ai besoin de savoir ce qui est changé dans le nouvel objet. Voici donc ma conception.

const oldState = {id:'170',name:'Ivab',secondName:'Ivanov',weight:45};
const newState = {id:'170',name:'Ivanko',secondName:'Ivanov',age:29};

const keysObj1 = R.keys(newState)

const filterFunc = key => {
  const value = R.eqProps(key,oldState,newState)
  return {[key]:value}
}

const result = R.map(filterFunc, keysObj1)

le résultat est, le nom de la propriété et son statut.

[{"id":true}, {"name":false}, {"secondName":true}, {"age":false}]
Ivan Titkov
la source
1

Voici une version dactylographiée du code @sbgoran

export class deepDiffMapper {

  static VALUE_CREATED = 'created';
  static VALUE_UPDATED = 'updated';
  static VALUE_DELETED = 'deleted';
  static VALUE_UNCHANGED ='unchanged';

  protected isFunction(obj: object) {
    return {}.toString.apply(obj) === '[object Function]';
  };

  protected isArray(obj: object) {
      return {}.toString.apply(obj) === '[object Array]';
  };

  protected isObject(obj: object) {
      return {}.toString.apply(obj) === '[object Object]';
  };

  protected isDate(obj: object) {
      return {}.toString.apply(obj) === '[object Date]';
  };

  protected isValue(obj: object) {
      return !this.isObject(obj) && !this.isArray(obj);
  };

  protected compareValues (value1: any, value2: any) {
    if (value1 === value2) {
        return deepDiffMapper.VALUE_UNCHANGED;
    }
    if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
        return deepDiffMapper.VALUE_UNCHANGED;
    }
    if ('undefined' == typeof(value1)) {
        return deepDiffMapper.VALUE_CREATED;
    }
    if ('undefined' == typeof(value2)) {
        return deepDiffMapper.VALUE_DELETED;
    }

    return deepDiffMapper.VALUE_UPDATED;
  }

  public map(obj1: object, obj2: object) {
      if (this.isFunction(obj1) || this.isFunction(obj2)) {
          throw 'Invalid argument. Function given, object expected.';
      }
      if (this.isValue(obj1) || this.isValue(obj2)) {
          return {
              type: this.compareValues(obj1, obj2),
              data: (obj1 === undefined) ? obj2 : obj1
          };
      }

      var diff = {};
      for (var key in obj1) {
          if (this.isFunction(obj1[key])) {
              continue;
          }

          var value2 = undefined;
          if ('undefined' != typeof(obj2[key])) {
              value2 = obj2[key];
          }

          diff[key] = this.map(obj1[key], value2);
      }
      for (var key in obj2) {
          if (this.isFunction(obj2[key]) || ('undefined' != typeof(diff[key]))) {
              continue;
          }

          diff[key] = this.map(undefined, obj2[key]);
      }

      return diff;

  }
}
wzr1337
la source
1

Voici une version modifiée de quelque chose trouvé sur gisthub .

isNullBlankOrUndefined = function (o) {
    return (typeof o === "undefined" || o == null || o === "");
}

/**
 * Deep diff between two object, using lodash
 * @param  {Object} object Object compared
 * @param  {Object} base   Object to compare with
 * @param  {Object} ignoreBlanks will not include properties whose value is null, undefined, etc.
 * @return {Object}        Return a new object who represent the diff
 */
objectDifference = function (object, base, ignoreBlanks = false) {
    if (!lodash.isObject(object) || lodash.isDate(object)) return object            // special case dates
    return lodash.transform(object, (result, value, key) => {
        if (!lodash.isEqual(value, base[key])) {
            if (ignoreBlanks && du.isNullBlankOrUndefined(value) && isNullBlankOrUndefined( base[key])) return;
            result[key] = lodash.isObject(value) && lodash.isObject(base[key]) ? objectDifference(value, base[key]) : value;
        }
    });
}
Nico
la source
1

J'ai modifié la réponse de @ sbgoran afin que l'objet diff résultant n'inclue que les valeurs modifiées et omet les valeurs identiques. En outre, il affiche à la fois la valeur d'origine et la valeur mise à jour .

var deepDiffMapper = function () {
    return {
        VALUE_CREATED: 'created',
        VALUE_UPDATED: 'updated',
        VALUE_DELETED: 'deleted',
        VALUE_UNCHANGED: '---',
        map: function (obj1, obj2) {
            if (this.isFunction(obj1) || this.isFunction(obj2)) {
                throw 'Invalid argument. Function given, object expected.';
            }
            if (this.isValue(obj1) || this.isValue(obj2)) {
                let returnObj = {
                    type: this.compareValues(obj1, obj2),
                    original: obj1,
                    updated: obj2,
                };
                if (returnObj.type != this.VALUE_UNCHANGED) {
                    return returnObj;
                }
                return undefined;
            }

            var diff = {};
            let foundKeys = {};
            for (var key in obj1) {
                if (this.isFunction(obj1[key])) {
                    continue;
                }

                var value2 = undefined;
                if (obj2[key] !== undefined) {
                    value2 = obj2[key];
                }

                let mapValue = this.map(obj1[key], value2);
                foundKeys[key] = true;
                if (mapValue) {
                    diff[key] = mapValue;
                }
            }
            for (var key in obj2) {
                if (this.isFunction(obj2[key]) || foundKeys[key] !== undefined) {
                    continue;
                }

                let mapValue = this.map(undefined, obj2[key]);
                if (mapValue) {
                    diff[key] = mapValue;
                }
            }

            //2020-06-13: object length code copied from https://stackoverflow.com/a/13190981/2336212
            if (Object.keys(diff).length > 0) {
                return diff;
            }
            return undefined;
        },
        compareValues: function (value1, value2) {
            if (value1 === value2) {
                return this.VALUE_UNCHANGED;
            }
            if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
                return this.VALUE_UNCHANGED;
            }
            if (value1 === undefined) {
                return this.VALUE_CREATED;
            }
            if (value2 === undefined) {
                return this.VALUE_DELETED;
            }
            return this.VALUE_UPDATED;
        },
        isFunction: function (x) {
            return Object.prototype.toString.call(x) === '[object Function]';
        },
        isArray: function (x) {
            return Object.prototype.toString.call(x) === '[object Array]';
        },
        isDate: function (x) {
            return Object.prototype.toString.call(x) === '[object Date]';
        },
        isObject: function (x) {
            return Object.prototype.toString.call(x) === '[object Object]';
        },
        isValue: function (x) {
            return !this.isObject(x) && !this.isArray(x);
        }
    }
}();
shieldgenerator7
la source
0

J'ai déjà écrit une fonction pour l'un de mes projets qui comparera un objet en tant qu'options utilisateur avec son clone interne. Il peut également valider et même remplacer par des valeurs par défaut si l'utilisateur a entré un mauvais type de données ou supprimé, en pur javascript.

Dans IE8 100% fonctionne. Testé avec succès.

//  ObjectKey: ["DataType, DefaultValue"]
reference = { 
    a : ["string", 'Defaul value for "a"'],
    b : ["number", 300],
    c : ["boolean", true],
    d : {
        da : ["boolean", true],
        db : ["string", 'Defaul value for "db"'],
        dc : {
            dca : ["number", 200],
            dcb : ["string", 'Default value for "dcb"'],
            dcc : ["number", 500],
            dcd : ["boolean", true]
      },
      dce : ["string", 'Default value for "dce"'],
    },
    e : ["number", 200],
    f : ["boolean", 0],
    g : ["", 'This is an internal extra parameter']
};

userOptions = { 
    a : 999, //Only string allowed
  //b : ["number", 400], //User missed this parameter
    c: "Hi", //Only lower case or case insitive in quotes true/false allowed.
    d : {
        da : false,
        db : "HelloWorld",
        dc : {
            dca : 10,
            dcb : "My String", //Space is not allowed for ID attr
            dcc: "3thString", //Should not start with numbers
            dcd : false
      },
      dce: "ANOTHER STRING",
    },
    e: 40,
    f: true,
};


function compare(ref, obj) {

    var validation = {
        number: function (defaultValue, userValue) {
          if(/^[0-9]+$/.test(userValue))
            return userValue;
          else return defaultValue;
        },
        string: function (defaultValue, userValue) {
          if(/^[a-z][a-z0-9-_.:]{1,51}[^-_.:]$/i.test(userValue)) //This Regex is validating HTML tag "ID" attributes
            return userValue;
          else return defaultValue;
        },
        boolean: function (defaultValue, userValue) {
          if (typeof userValue === 'boolean')
            return userValue;
          else return defaultValue;
        }
    };

    for (var key in ref)
        if (obj[key] && obj[key].constructor && obj[key].constructor === Object)
          ref[key] = compare(ref[key], obj[key]);
        else if(obj.hasOwnProperty(key))
          ref[key] = validation[ref[key][0]](ref[key][1], obj[key]); //or without validation on user enties => ref[key] = obj[key]
        else ref[key] = ref[key][1];
    return ref;
}

//console.log(
    alert(JSON.stringify( compare(reference, userOptions),null,2 ))
//);

/* résultat

{
  "a": "Defaul value for \"a\"",
  "b": 300,
  "c": true,
  "d": {
    "da": false,
    "db": "Defaul value for \"db\"",
    "dc": {
      "dca": 10,
      "dcb": "Default value for \"dcb\"",
      "dcc": 500,
      "dcd": false
    },
    "dce": "Default value for \"dce\""
  },
  "e": 40,
  "f": true,
  "g": "This is an internal extra parameter"
}

*/
Samad Aghaei
la source
0

La fonction plus étendue et simplifiée de la réponse de sbgoran.
Cela permet une analyse approfondie et de trouver la simillarité d'un tableau.

var result = objectDifference({
      a:'i am unchanged',
      b:'i am deleted',
      e: {a: 1,b:false, c: null},
      f: [1,{a: 'same',b:[{a:'same'},{d: 'delete'}]}],
      g: new Date('2017.11.25'),
      h: [1,2,3,4,5]
  },
  {
      a:'i am unchanged',
      c:'i am created',
      e: {a: '1', b: '', d:'created'},
      f: [{a: 'same',b:[{a:'same'},{c: 'create'}]},1],
      g: new Date('2017.11.25'),
      h: [4,5,6,7,8]
  });
console.log(result);

function objectDifference(obj1, obj2){
    if((dataType(obj1) !== 'array' && dataType(obj1) !== 'object') || (dataType(obj2) !== 'array' && dataType(obj2) !== 'object')){
        var type = '';

        if(obj1 === obj2 || (dataType(obj1) === 'date' && dataType(obj2) === 'date' && obj1.getTime() === obj2.getTime()))
            type = 'unchanged';
        else if(dataType(obj1) === 'undefined')
            type = 'created';
        if(dataType(obj2) === 'undefined')
            type = 'deleted';
        else if(type === '') type = 'updated';

        return {
            type: type,
            data:(obj1 === undefined) ? obj2 : obj1
        };
    }
  
    if(dataType(obj1) === 'array' && dataType(obj2) === 'array'){
        var diff = [];
        obj1.sort(); obj2.sort();
        for(var i = 0; i < obj2.length; i++){
            var type = obj1.indexOf(obj2[i]) === -1?'created':'unchanged';
            if(type === 'created' && (dataType(obj2[i]) === 'array' || dataType(obj2[i]) === 'object')){
                diff.push(
                    objectDifference(obj1[i], obj2[i])
                );
                continue;
            }
            diff.push({
                type: type,
                data: obj2[i]
            });
        }

        for(var i = 0; i < obj1.length; i++){
            if(obj2.indexOf(obj1[i]) !== -1 || dataType(obj1[i]) === 'array' || dataType(obj1[i]) === 'object')
                continue;
            diff.push({
                type: 'deleted',
                data: obj1[i]
            });
        }
    } else {
        var diff = {};
        var key = Object.keys(obj1);
        for(var i = 0; i < key.length; i++){
            var value2 = undefined;
            if(dataType(obj2[key[i]]) !== 'undefined')
                value2 = obj2[key[i]];

            diff[key[i]] = objectDifference(obj1[key[i]], value2);
        }

        var key = Object.keys(obj2);
        for(var i = 0; i < key.length; i++){
            if(dataType(diff[key[i]]) !== 'undefined')
                continue;

            diff[key[i]] = objectDifference(undefined, obj2[key[i]]);
        }
    }

    return diff;
}

function dataType(data){
    if(data === undefined || data === null) return 'undefined';
    if(data.constructor === String) return 'string';
    if(data.constructor === Array) return 'array';
    if(data.constructor === Object) return 'object';
    if(data.constructor === Number) return 'number';
    if(data.constructor === Boolean) return 'boolean';
    if(data.constructor === Function) return 'function';
    if(data.constructor === Date) return 'date';
    if(data.constructor === RegExp) return 'regex';
    return 'unknown';
}

StefansArya
la source
0

Je suis tombé ici en essayant de chercher un moyen de faire la différence entre deux objets. Voici ma solution en utilisant Lodash:

// Get updated values (including new values)
var updatedValuesIncl = _.omitBy(curr, (value, key) => _.isEqual(last[key], value));

// Get updated values (excluding new values)
var updatedValuesExcl = _.omitBy(curr, (value, key) => (!_.has(last, key) || _.isEqual(last[key], value)));

// Get old values (by using updated values)
var oldValues = Object.keys(updatedValuesIncl).reduce((acc, key) => { acc[key] = last[key]; return acc; }, {});

// Get newly added values
var newCreatedValues = _.omitBy(curr, (value, key) => _.has(last, key));

// Get removed values
var deletedValues = _.omitBy(last, (value, key) => _.has(curr, key));

// Then you can group them however you want with the result

Extrait de code ci-dessous:

var last = {
"authed": true,
"inForeground": true,
"goodConnection": false,
"inExecutionMode": false,
"online": true,
"array": [1, 2, 3],
"deep": {
	"nested": "value",
},
"removed": "value",
};

var curr = {
"authed": true,
"inForeground": true,
"deep": {
	"nested": "changed",
},
"array": [1, 2, 4],
"goodConnection": true,
"inExecutionMode": false,
"online": false,
"new": "value"
};

// Get updated values (including new values)
var updatedValuesIncl = _.omitBy(curr, (value, key) => _.isEqual(last[key], value));
// Get updated values (excluding new values)
var updatedValuesExcl = _.omitBy(curr, (value, key) => (!_.has(last, key) || _.isEqual(last[key], value)));
// Get old values (by using updated values)
var oldValues = Object.keys(updatedValuesIncl).reduce((acc, key) => { acc[key] = last[key]; return acc; }, {});
// Get newly added values
var newCreatedValues = _.omitBy(curr, (value, key) => _.has(last, key));
// Get removed values
var deletedValues = _.omitBy(last, (value, key) => _.has(curr, key));

console.log('oldValues', JSON.stringify(oldValues));
console.log('updatedValuesIncl', JSON.stringify(updatedValuesIncl));
console.log('updatedValuesExcl', JSON.stringify(updatedValuesExcl));
console.log('newCreatedValues', JSON.stringify(newCreatedValues));
console.log('deletedValues', JSON.stringify(deletedValues));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js"></script>

Jee Mok
la source
0

J'ai pris la réponse ci-dessus par @sbgoran et l'ai modifiée pour mon cas comme la question nécessaire, pour traiter les tableaux comme des ensembles (c'est-à-dire que l'ordre n'est pas important pour diff)

const deepDiffMapper = function () {
return {
  VALUE_CREATED: "created",
  VALUE_UPDATED: "updated",
  VALUE_DELETED: "deleted",
  VALUE_UNCHANGED: "unchanged",
  map: function(obj1: any, obj2: any) {
    if (this.isFunction(obj1) || this.isFunction(obj2)) {
      throw "Invalid argument. Function given, object expected.";
    }
    if (this.isValue(obj1) || this.isValue(obj2)) {
      return {
        type: this.compareValues(obj1, obj2),
        data: obj2 === undefined ? obj1 : obj2
      };
    }

    if (this.isArray(obj1) || this.isArray(obj2)) {
      return {
        type: this.compareArrays(obj1, obj2),
        data: this.getArrayDiffData(obj1, obj2)
      };
    }

    const diff: any = {};
    for (const key in obj1) {

      if (this.isFunction(obj1[key])) {
        continue;
      }

      let value2 = undefined;
      if (obj2[key] !== undefined) {
        value2 = obj2[key];
      }

      diff[key] = this.map(obj1[key], value2);
    }
    for (const key in obj2) {
      if (this.isFunction(obj2[key]) || diff[key] !== undefined) {
        continue;
      }

      diff[key] = this.map(undefined, obj2[key]);
    }

    return diff;

  },

  getArrayDiffData: function(arr1: Array<any>, arr2: Array<any>) {
    const set1 = new Set(arr1);
    const set2 = new Set(arr2);

    if (arr1 === undefined || arr2 === undefined) {
       return arr1 === undefined ? arr1 : arr2;
    }
    const deleted = [...arr1].filter(x => !set2.has(x));

    const added = [...arr2].filter(x => !set1.has(x));

    return {
      added, deleted
    };

  },

  compareArrays: function(arr1: Array<any>, arr2: Array<any>) {
    const set1 = new Set(arr1);
    const set2 = new Set(arr2);
    if (_.isEqual(_.sortBy(arr1), _.sortBy(arr2))) {
      return this.VALUE_UNCHANGED;
    }
    if (arr1 === undefined) {
      return this.VALUE_CREATED;
    }
    if (arr2 === undefined) {
      return this.VALUE_DELETED;
    }
    return this.VALUE_UPDATED;
  },
  compareValues: function (value1: any, value2: any) {
    if (value1 === value2) {
      return this.VALUE_UNCHANGED;
    }
    if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
      return this.VALUE_UNCHANGED;
    }
    if (value1 === undefined) {
      return this.VALUE_CREATED;
    }
    if (value2 === undefined) {
      return this.VALUE_DELETED;
    }
    return this.VALUE_UPDATED;
  },
  isFunction: function (x: any) {
    return Object.prototype.toString.call(x) === "[object Function]";
  },
  isArray: function (x: any) {
    return Object.prototype.toString.call(x) === "[object Array]";
  },
  isDate: function (x: any) {
    return Object.prototype.toString.call(x) === "[object Date]";
  },
  isObject: function (x: any) {
    return Object.prototype.toString.call(x) === "[object Object]";
  },
  isValue: function (x: any) {
    return !this.isObject(x) && !this.isArray(x);
  }
 };
}();
Bashar Ali Labadi
la source
0

Voici une solution qui est:

  • Typographie (mais facilement convertible en Javascript)
  • ne pas avoir de dépendances lib
  • générique, et ne se soucie pas de vérifier les types d'objets (à part le objecttype)
  • prend en charge les propriétés avec valeur undefined
  • profond ou non (par défaut)

Nous définissons d'abord l'interface du résultat de la comparaison:

export interface ObjectComparison {
  added: {};
  updated: {
    [propName: string]: Change;
  };
  removed: {};
  unchanged: {};
}

avec le cas particulier du changement où nous voulons savoir quelles sont les anciennes et les nouvelles valeurs:

export interface Change {
  oldValue: any;
  newValue: any;
}

Ensuite, nous pouvons fournir la difffonction qui n'est que deux boucles (avec récursivité si deepc'est le cas true):

export class ObjectUtils {

  static diff(o1: {}, o2: {}, deep = false): ObjectComparison {
    const added = {};
    const updated = {};
    const removed = {};
    const unchanged = {};
    for (const prop in o1) {
      if (o1.hasOwnProperty(prop)) {
        const o2PropValue = o2[prop];
        const o1PropValue = o1[prop];
        if (o2.hasOwnProperty(prop)) {
          if (o2PropValue === o1PropValue) {
            unchanged[prop] = o1PropValue;
          } else {
            updated[prop] = deep && this.isObject(o1PropValue) && this.isObject(o2PropValue) ? this.diff(o1PropValue, o2PropValue, deep) : {newValue: o2PropValue};
          }
        } else {
          removed[prop] = o1PropValue;
        }
      }
    }
    for (const prop in o2) {
      if (o2.hasOwnProperty(prop)) {
        const o1PropValue = o1[prop];
        const o2PropValue = o2[prop];
        if (o1.hasOwnProperty(prop)) {
          if (o1PropValue !== o2PropValue) {
            if (!deep || !this.isObject(o1PropValue)) {
              updated[prop].oldValue = o1PropValue;
            }
          }
        } else {
          added[prop] = o2PropValue;
        }
      }
    }
    return { added, updated, removed, unchanged };
  }

  /**
   * @return if obj is an Object, including an Array.
   */
  static isObject(obj: any) {
    return obj !== null && typeof obj === 'object';
  }
}

Par exemple, en appelant:

ObjectUtils.diff(
  {
    a: 'a', 
    b: 'b', 
    c: 'c', 
    arr: ['A', 'B'], 
    obj: {p1: 'p1', p2: 'p2'}
  },
  {
    b: 'x', 
    c: 'c', 
    arr: ['B', 'C'], 
    obj: {p2: 'p2', p3: 'p3'}, 
    d: 'd'
  },
);

retournera:

{
  added: {d: 'd'},
  updated: {
    b: {oldValue: 'b', newValue: 'x'},
    arr: {oldValue: ['A', 'B'], newValue: ['B', 'C']},
    obj: {oldValue: {p1: 'p1', p2: 'p2'}, newValue: {p2: 'p2', p3: 'p3'}}
  },
  removed: {a: 'a'},
  unchanged: {c: 'c'},
}

et l'appelant avec le deeptroisième paramètre renverra:

{
  added: {d: 'd'},
  updated: {
    b: {oldValue: 'b', newValue: 'x'},
    arr: {
      added: {},
      removed: {},
      unchanged: {},
      updated: {
        0: {oldValue: 'A', newValue: 'B'},
        1: {oldValue: 'B', newValue: 'C', }
      }
    },
    obj: {
      added: {p3: 'p3'},
      removed: {p1: 'p1'},
      unchanged: {p2: 'p2'},
      updated: {}
    }
  },
  removed: {a: 'a'},
  unchanged: {c: 'c'},
}
Javarome
la source