Generisches Deep Diff zwischen zwei Objekten


222

Ich habe zwei Objekte: oldObjund newObj.

Die Daten in oldObjwurden zum Auffüllen eines Formulars verwendet und newObjsind das Ergebnis der Änderung und Übermittlung der Daten in diesem Formular durch den Benutzer.

Beide Objekte sind tief, dh. Sie haben Eigenschaften, die Objekte oder Arrays von Objekten usw. sind. Sie können n Ebenen tief sein, daher muss der Diff-Algorithmus rekursiv sein.

Jetzt muss ich nicht nur herauszufinden , was geändert wurde (wie in hinzugefügt / aktualisiert / gelöscht) aus oldObjzu newObj, sondern auch , wie man am besten repräsentieren sie.

Bisher dachte ich nur daran, eine genericDeepDiffBetweenObjectsMethode zu erstellen , die ein Objekt auf dem Formular {add:{...},upd:{...},del:{...}}zurückgibt, aber dann dachte ich: Jemand anderes muss das schon einmal gebraucht haben.

Also ... kennt jemand eine Bibliothek oder einen Code, der dies tut und vielleicht eine noch bessere Möglichkeit hat, den Unterschied darzustellen (auf eine Weise, die immer noch JSON-serialisierbar ist)?

Aktualisieren:

Ich habe mir eine bessere Möglichkeit überlegt, die aktualisierten Daten darzustellen, indem ich dieselbe Objektstruktur wie verwende newObj, aber alle Eigenschaftswerte in Objekte im Formular umwandle:

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

Also wenn newObj.prop1 = 'new value'und oldObj.prop1 = 'old value'es würde setzenreturnObj.prop1 = {type: 'update', data: 'new value'}

Update 2:

Es wird wirklich haarig, wenn wir zu Eigenschaften kommen, die Arrays sind, da das Array [1,2,3]als gleich gezählt werden sollte [2,3,1], was für Arrays von wertbasierten Typen wie string, int & bool einfach genug ist, aber wirklich schwierig zu handhaben ist, wenn es darum geht Arrays von Referenztypen wie Objekte und Arrays.

Beispielarrays, die gleich gefunden werden sollten:

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

Es ist nicht nur recht komplex, nach dieser Art von tiefer Wertgleichheit zu suchen, sondern auch einen guten Weg zu finden, um die möglichen Änderungen darzustellen.



2
@ a'r: Es ist kein Duplikat von stackoverflow.com/questions/1200562/… - Ich weiß, wie man die Objekte durchquert. Ich suche nach dem Stand der Technik, da dies nicht trivial ist und die Implementierung in Echtzeit dauert Ich würde lieber eine Bibliothek verwenden, als sie von Grund auf neu zu erstellen.
Martin Jespersen

1
Benötigen Sie wirklich unterschiedliche Objekte? Wird das newObj vom Server auf der Formularübermittlungsantwort generiert? Denn wenn Sie keine "Server-Updates" eines Objekts haben, können Sie Ihr Problem vereinfachen, indem Sie geeignete Ereignis-Listener anhängen und bei Benutzerinteraktion (Objektänderung) die gewünschte Änderungsliste aktualisieren / generieren.
sbgoran

1
@sbgoran: newObjwird durch js Code generiert, der Werte aus einem Formular im DOM liest. Es gibt verschiedene Möglichkeiten, den Zustand beizubehalten und dies viel einfacher zu tun, aber ich möchte ihn als Übung staatenlos halten. Ich suche auch nach dem Stand der Technik, um zu sehen, wie andere dies angegangen sein könnten, wenn es tatsächlich jemand getan hat.
Martin Jespersen

3
Hier ist eine sehr ausgefeilte Bibliothek, mit der Sie jedes Paar von Javascript-Objekten unterscheiden / patchen können. github.com/benjamine/jsondiffpatch Sie können sie hier live sehen: benjamine.github.io/jsondiffpatch/demo/index.html (Haftungsausschluss: Ich bin der Autor)
Benja

Antworten:


141

Ich habe eine kleine Klasse geschrieben, die macht, was Sie wollen. Sie können sie hier testen .

Das einzige, was sich von Ihrem Vorschlag unterscheidet, ist, dass ich es nicht als [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]gleich betrachte, weil ich denke, dass Arrays nicht gleich sind, wenn die Reihenfolge ihrer Elemente nicht gleich ist. Dies kann natürlich bei Bedarf geändert werden. Auch dieser Code kann weiter verbessert werden, um als Argument zu fungieren, das zum willkürlichen Formatieren von diff-Objekten basierend auf übergebenen primitiven Werten verwendet wird (jetzt wird diese Aufgabe mit der Methode "compareValues" ausgeführt).

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


3
+1 Es ist kein schlechter Code. Es gibt jedoch einen Fehler ( siehe Beispiel: jsfiddle.net/kySNu/3c wird als Zeichenfolge erstellt undefined, sollte es aber sein 'i am created'), und außerdem macht es nicht das, was ich brauche, da es an dem Vergleich des tiefen Array-Werts mangelt wichtigster (und komplexester / schwieriger) Teil. Als Randnotiz ist das Konstrukt 'array' != typeof(obj)nutzlos, da Arrays Objekte sind, die Instanzen von Arrays sind.
Martin Jespersen

1
Ich habe den Code aktualisiert, bin mir aber nicht sicher, welchen Wert Sie für das resultierende Objekt benötigen. Derzeit gibt der Code den Wert des ersten Objekts zurück. Wenn er nicht vorhanden ist, wird der Wert des zweiten Objekts als Daten festgelegt.
sbgoran

1
Und wie meinst du "Fehlen des Deep Array Value Compare" für Arrays, die Sie für jeden Index dieses {type: ..., data:..}Objekts erhalten. Was fehlt, ist die Suche nach dem Wert des ersten Arrays im zweiten, aber wie ich in meiner Antwort erwähnt habe, denke ich nicht, dass Arrays gleich sind, wenn die Reihenfolge ihrer Werte nicht gleich ist ( [1, 2, 3] is not equal to [3, 2, 1]meiner Meinung nach).
sbgoran

6
@MartinJespersen OK, wie würden Sie diese Arrays dann generisch behandeln : [{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]. Jetzt wird das erste Objekt im ersten Array mit "Wert1" oder "Wert2" aktualisiert. Und dies ist ein einfaches Beispiel, es könnte beim tiefen Verschachteln sehr kompliziert werden. Wenn Sie einen tiefen Verschachtelungsvergleich unabhängig von der Schlüsselposition wünschen / benötigen, erstellen Sie keine Arrays von Objekten. Erstellen Sie Objekte mit verschachtelten Objekten wie im vorherigen Beispiel : {inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}.
sbgoran

2
Ich stimme Ihrer letzten Sichtweise zu - die ursprüngliche Datenstruktur sollte in etwas geändert werden, das einfacher ist, einen tatsächlichen Unterschied zu machen. Herzlichen Glückwunsch, Sie haben es geschafft :)
Martin Jespersen

88

Mit Unterstrich ein einfacher Unterschied:

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

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

Die Ergebnisse in den Teilen o1entsprechen, jedoch mit unterschiedlichen Werten in o2:

{a: 1, b: 2}

Für einen tiefen Unterschied wäre es anders:

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

Wie von @Juhana in den Kommentaren hervorgehoben, ist das Obige nur ein Unterschied a -> b und nicht reversibel (was bedeutet, dass zusätzliche Eigenschaften in b ignoriert würden). Verwenden Sie stattdessen 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());

Ein vollständiges Beispiel + Tests + Mixins finden Sie unter http://jsfiddle.net/drzaus/9g5qoxwj/


Sie waren sich nicht sicher, warum Sie abgelehnt wurden. Dies war ausreichend, da Sie ein flaches, einfaches Beispiel sowie eine komplexere Tiefenfunktion lieferten.
Seiyria

2
@Seiyria Hasser werden hassen, denke ich ... Ich habe beides getan, weil ich ursprünglich dachte, es omitwäre ein tiefer Unterschied, aber es war falsch, also auch zum Vergleich.
Drzaus

1
Schöne Lösung. Ich würde vorschlagen , ändern r[k] = ... : vin r[k] = ... : {'a':v, 'b':b[k] }, auf diese Weise Sie zwei Werte sehen.
Guyaloni

2
Beide geben ein falsches Negativ zurück, wenn die Objekte ansonsten identisch sind, das zweite jedoch mehr Elemente enthält, z . B. {a:1, b:2}und {a:1, b:2, c:3}.
JJJ

1
Es sollte _.omitBystatt sein _.omit.
JP

48

Ich möchte eine ES6-Lösung anbieten ... Dies ist ein Einweg-Diff, was bedeutet, dass Schlüssel / Werte zurückgegeben werden o2, die nicht mit denen in identisch sind in 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]
  }
}, {})

3
Schöne Lösung, aber vielleicht möchten Sie diese if(o1[key] === o1[key])Linie Kerl überprüfen
bm_i

Ist der Code vollständig? Ich bekommeUncaught SyntaxError: Unexpected token ...
Seano

2
Ich mag die Lösung, aber es gibt ein Problem: Wenn das Objekt tiefer als eine Ebene ist, werden alle Werte in den geänderten verschachtelten Objekten zurückgegeben - oder zumindest passiert das für mich.
Falsche

3
Ja, das ist nicht rekursiv @Spurious
Nemesarial

2
Denken Sie daran, dass Sie mit dieser Lösung für jedes Element im Objekt ein völlig neues Objekt erhalten, in das alle vorhandenen Elemente kopiert wurden, um nur ein Element zum Array hinzuzufügen. Für kleine Objekte ist es in Ordnung, für größere Objekte wird es jedoch exponentiell langsamer.
Malvineous

22

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

Ich verwende keinen Schlüssel / Objekt / Quelle, aber ich habe ihn dort belassen, wenn Sie darauf zugreifen müssen. Der Objektvergleich verhindert lediglich, dass die Konsole die Unterschiede vom äußersten zum innersten Element auf die Konsole druckt.

Sie können eine Logik hinzufügen, um Arrays zu verarbeiten. Vielleicht sortieren Sie zuerst die Arrays. Dies ist eine sehr flexible Lösung.

BEARBEITEN

Geändert von _.merge zu _.mergeWith aufgrund eines lodash-Updates. Vielen Dank an Aviron, dass Sie die Änderung bemerkt haben.


6
In lodash 4.15.0 wird _.merge mit Customizer-Funktion nicht mehr unterstützt, daher sollten Sie stattdessen _.mergeWith verwenden.
Aviran Cohen

1
Diese Funktion ist großartig, funktioniert aber nicht in verschachtelten Objekten.
Joe Allen

13

Hier ist eine JavaScript-Bibliothek, mit der Sie Unterschiede zwischen zwei JavaScript-Objekten finden können:

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

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

Sie können die rekursive Diff-Bibliothek sowohl im Browser als auch in Node.js verwenden. Gehen Sie für den Browser wie folgt vor:

<script type="text" src="https://unpkg.com/recursive-diff@1.0.0/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>

In node.js können Sie das Modul 'recursive-diff' benötigen und es wie folgt verwenden:

const diff = require('recursive-diff');
const ob1 = {a: 1}, ob2: {b:2};
const diff = diff.getDiff(ob1, ob2);

Dies berücksichtigt beispielsweise keine Änderungen der Datumseigenschaften.
Trollkotze


9

Heutzutage stehen dafür einige Module zur Verfügung. Ich habe kürzlich ein Modul dazu geschrieben, weil ich mit den zahlreichen unterschiedlichen Modulen, die ich gefunden habe, nicht zufrieden war. Es heißt odiff: https://github.com/Tixit/odiff . Ich habe auch eine Reihe der beliebtesten Module aufgelistet und erklärt, warum sie in der Readme-Datei von nicht akzeptabel waren. Sie können sie durchsehen odiff, wenn odiffsie nicht die gewünschten Eigenschaften haben. Hier ist ein Beispiel:

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

7
const diff = require("deep-object-diff").diff;
let differences = diff(obj2, obj1);

Es gibt ein npm-Modul mit über 500.000 wöchentlichen Downloads: https://www.npmjs.com/package/deep-object-diff

Ich mag die objektähnliche Darstellung der Unterschiede - besonders ist es leicht, die Struktur zu sehen, wenn sie formatiert ist.

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

2

Ich habe diesen Code für die von Ihnen beschriebene Aufgabe verwendet:

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

Dadurch erhalten Sie ein neues Objekt, das alle Änderungen zwischen dem alten und dem neuen Objekt aus Ihrem Formular zusammenführt


1
Ich verwende das Ext-Framework hier, aber Sie können es ersetzen und
jedes

Das Zusammenführen von Objekten ist trivial und kann so einfach wie die $.extend(true,obj1,obj2)Verwendung von jQuery durchgeführt werden. Das brauche ich überhaupt nicht. Ich brauche den Unterschied zwischen den beiden Objekten, nicht die Kombination von ihnen.
Martin Jespersen

Es ist großartig, dass Ext hier verwendet wird
Peroxid

2

Ich habe die Funktion "compareValue ()" in Javascript entwickelt. Es wird zurückgegeben, ob der Wert gleich ist oder nicht. Ich habe compareValue () in der for-Schleife eines Objekts aufgerufen. In diffParams können Sie zwei Objekte unterscheiden.

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


1

Ich weiß, dass ich zu spät zur Party komme, aber ich brauchte etwas Ähnliches, dass die obigen Antworten nicht geholfen haben.

Ich habe Angulars $ watch-Funktion verwendet, um Änderungen in einer Variablen zu erkennen. Ich musste nicht nur wissen, ob sich eine Eigenschaft für die Variable geändert hat, sondern auch sicherstellen, dass die geänderte Eigenschaft kein temporäres, berechnetes Feld ist. Mit anderen Worten, ich wollte bestimmte Eigenschaften ignorieren.

Hier ist der Code: https://jsfiddle.net/rv01x6jo/

So verwenden Sie es:

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

Hoffe das hilft jemandem.


Bitte geben Sie den Code auch in Ihre Antwort ein, nicht nur eine Geige.
xpy

Es scheint, als würde defineProperty dieses Problem mit besserer Leistung lösen. Wenn ich mich richtig erinnere, funktioniert es bis hinunter zu IE9.
Peter

Vielen Dank..!! Ihr Code funktioniert wie ein Zauber und hat meinen Tag gerettet. Ich habe ein json-Objekt mit 1250 Zeilen und es gibt mir genau das O / P, das ich will.
Tejas Mehta

1

Ich benutze nur Ramda, um das gleiche Problem zu lösen, muss ich wissen, was in neuen Objekten geändert wird. Also hier mein Design.

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)

Ergebnis ist, Name der Eigenschaft und deren Status.

[{"id":true}, {"name":false}, {"secondName":true}, {"age":false}]

1

Hier ist eine maschinengeschriebene Version des @ sbgoran-Codes

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;

  }
}

1

Hier ist eine modifizierte Version von etwas, das auf gisthub gefunden wurde .

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

1

Ich habe die Antwort von @ sbgoran so geändert, dass das resultierende Diff-Objekt nur die geänderten Werte enthält und dieselben Werte weglässt. Außerdem werden sowohl der ursprüngliche als auch der aktualisierte Wert angezeigt .

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

0

Ich habe bereits eine Funktion für eines meiner Projekte geschrieben, mit der ein Objekt als Benutzeroption mit seinem internen Klon verglichen wird. Es kann auch Standardwerte validieren und sogar ersetzen, wenn der Benutzer einen falschen Datentyp eingegeben oder in reinem Javascript entfernt hat.

In IE8 funktioniert 100%. Erfolgreich getestet.

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

/ * Ergebnis

{
  "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"
}

*/

0

Die erweiterte und vereinfachte Funktion aus sbgorans Antwort.
Dies ermöglicht ein tiefes Scannen und das Finden der Ähnlichkeit eines Arrays.

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


0

Ich bin hier gestolpert und habe versucht, einen Weg zu finden, um den Unterschied zwischen zwei Objekten zu erkennen. Dies ist meine Lösung mit 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

Code-Snippet unten:

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>


0

Ich nahm die obige Antwort von @sbgoran und änderte sie für meinen Fall genauso wie die benötigte Frage, um Arrays als Mengen zu behandeln (dh die Reihenfolge ist für diff nicht wichtig).

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

0

Hier ist eine Lösung:

  • Typoskript (aber leicht in Javascript konvertierbar)
  • habe keine lib-Abhängigkeiten
  • generisch und kümmert sich nicht um die Überprüfung von Objekttypen (abgesehen vom objectTyp)
  • unterstützt Eigenschaften mit Wert undefined
  • tief von nicht (Standard)

Zuerst definieren wir die Schnittstelle für Vergleichsergebnisse:

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

mit dem speziellen Fall der Veränderung, in dem wir wissen wollen, was alte und neue Werte sind:

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

Dann können wir die diffFunktion bereitstellen, die lediglich zwei Schleifen sind (mit Rekursivität, wenn dies der Fall deepist 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';
  }
}

Als Beispiel rufen Sie an:

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

wird zurückkehren:

{
  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'},
}

Wenn Sie dasselbe mit dem deepdritten Parameter aufrufen, wird Folgendes zurückgegeben:

{
  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'},
}

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.