Ogólna głęboka różnica między dwoma obiektami


222

Mam dwa przedmioty: oldObji newObj.

Dane oldObjzostały użyte do wypełnienia formularza i newObjsą wynikiem zmiany danych użytkownika w tym formularzu i przesłania go.

Oba obiekty są głębokie, tj. mają właściwości, które są obiektami lub tablicami obiektów itp. - mogą mieć głębokość n poziomów, dlatego algorytm różnicy musi być rekurencyjny.

Teraz muszę nie tylko dowiedzieć się, co zostało zmienione (jak dodano / zaktualizowano / usunąć) od oldObjdo newObj, ale także jak najlepiej to przedstawić.

Do tej pory myślałem o zbudowaniu genericDeepDiffBetweenObjectsmetody, która zwróciłaby obiekt na formularzu, {add:{...},upd:{...},del:{...}}ale potem pomyślałem: ktoś inny musiał tego wcześniej potrzebować.

Więc ... czy ktoś wie o bibliotece lub fragmencie kodu, który to zrobi i może ma jeszcze lepszy sposób reprezentowania różnicy (w sposób, który nadal jest możliwy do serializacji przez JSON)?

Aktualizacja:

Pomyślałem o lepszym sposobie reprezentacji zaktualizowanych danych, używając tej samej struktury obiektu newObj, ale przekształcając wszystkie wartości właściwości w obiekty w formularzu:

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

Więc jeśli newObj.prop1 = 'new value'i oldObj.prop1 = 'old value'to się ustawireturnObj.prop1 = {type: 'update', data: 'new value'}

Aktualizacja 2:

Robi się naprawdę owłosiony, gdy dochodzimy do właściwości, które są tablicami, ponieważ tablica [1,2,3]powinna być liczona jako równa [2,3,1], co jest dość proste dla tablic typów opartych na wartościach, takich jak string, int & bool, ale naprawdę trudno jest sobie z tym poradzić, jeśli chodzi o tablice typów referencyjnych, takich jak obiekty i tablice.

Przykładowe tablice, które należy uznać za równe:

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

Sprawdzenie tego rodzaju głębokiej równości wartości jest nie tylko dość skomplikowane, ale również wymyślenie dobrego sposobu przedstawienia potencjalnych zmian.



2
@ a'r: To nie jest duplikat stackoverflow.com/questions/1200562/... - Wiem, jak przemierzać obiekty, szukam stanu techniki, ponieważ nie jest to trywialne i jego wdrożenie zajmie w czasie rzeczywistym, a ja wolałbym użyć biblioteki niż zrobić ją od zera.
Martin Jespersen,

1
Czy naprawdę potrzebujesz diff obiektów, czy newObj jest generowany z serwera w formularzu odpowiedzi? Ponieważ jeśli nie masz „aktualizacji serwera” obiektu, możesz uprościć swój problem, dołączając odpowiednie detektory zdarzeń i po interakcji użytkownika (zmiana obiektu) możesz zaktualizować / wygenerować listę żądanych zmian.
sgorgor

1
@sbgoran: newObjjest generowany przez odczyt kodu js z formularza w DOM. Istnieje kilka sposobów, aby utrzymać stan i zrobić to o wiele łatwiej, ale chciałbym, aby ćwiczenie to było bezstanowe. Szukam również stanu techniki, aby zobaczyć, jak inni mogliby rozwiązać ten problem, jeśli w rzeczywistości ktokolwiek to zrobił.
Martin Jespersen,

3
oto bardzo wyrafinowana biblioteka do różnicowania / łatania dowolnej pary obiektów JavaScript github.com/benjamine/jsondiffpatch, którą można zobaczyć na żywo tutaj: benjamine.github.io/jsondiffpatch/demo/index.html (zrzeczenie się: jestem autorem)
Benja,

Odpowiedzi:


141

Napisałem małą klasę, która robi to, co chcesz, możesz to przetestować tutaj .

Jedyną rzeczą, która różni się od twojej propozycji, jest to, że nie uważam [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]tego samego, ponieważ uważam, że tablice nie są równe, jeśli kolejność ich elementów nie jest taka sama. Oczywiście można to zmienić w razie potrzeby. Również ten kod można dodatkowo ulepszyć, aby pełnił funkcję argumentu, który zostanie użyty do sformatowania obiektu diff w dowolny sposób na podstawie przekazanych prymitywnych wartości (teraz to zadanie jest wykonywane metodą „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);


3
+1 To niezły kawałek kodu. Wystąpił jednak błąd (sprawdź ten przykład: jsfiddle.net/kySNu/3 c jest tworzony jako, undefinedale powinien być ciągiem 'i am created'), a poza tym nie robi tego, czego potrzebuję, ponieważ brakuje mu głębokiej wartości porównania, która jest najważniejsza (i złożona / trudna) część. Na marginesie, konstrukcja 'array' != typeof(obj)jest bezużyteczna, ponieważ tablice są obiektami, które są instancjami tablic.
Martin Jespersen,

1
Zaktualizowałem kod, ale nie jestem pewien, jakiej wartości chcesz w obiekcie wynikowym, teraz kod zwraca wartość z pierwszego obiektu, a jeśli nie istnieje, wartość z drugiego obiektu zostanie ustawiona jako dane.
sbgoran

1
I co masz na myśli mówiąc „brak porównania wartości głębokiej tablicy” dla tablic, które otrzymasz za każdy indeks tego {type: ..., data:..}obiektu. Brakuje przeszukiwania wartości z pierwszej tablicy na sekundę, ale jak wspomniałem w mojej odpowiedzi, nie sądzę, że tablice są równe, jeśli kolejność ich wartości nie jest równa ( [1, 2, 3] is not equal to [3, 2, 1]moim zdaniem).
sbgoran

6
@MartinJespersen OK, jak można ogólnie traktować to tablice następnie: [{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]. Teraz pierwszy obiekt w pierwszej tablicy został zaktualizowany o „wartość1” lub „wartość2”. I to jest prosty przykład, może się skomplikować przy głębokim zagnieżdżaniu. Jeśli chcesz / potrzebujesz głęboko gniazdowania porównania niezależnie od pozycji klucza nie tworzą tablicę obiektów, tworzenie obiektów zagnieżdżonych obiektów, takich jak dla poprzedniego przykładu: {inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}.
sgorgor

2
Zgadzam się z tobą z ostatniego punktu widzenia - oryginalna struktura danych powinna zostać zmieniona na coś, co jest łatwiejsze do zrobienia. Gratulacje, przybiliście go :)
Martin Jespersen

88

Używając podkreślenia, prosty 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; })

Wyniki w częściach, o1które odpowiadają, ale z różnymi wartościami w o2:

{a: 1, b: 2}

Byłoby inaczej dla głębokiego różnicy:

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

Jak wskazał @Juhana w komentarzach, powyższe jest tylko różnicą a -> b i nie jest odwracalne (co oznacza, że ​​dodatkowe właściwości w b zostaną zignorowane). Zamiast tego użyj 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());

Zobacz http://jsfiddle.net/drzaus/9g5qoxwj/ dla pełnego przykładu + testy + mixiny


Nie jestem pewien, dlaczego zostałeś przegłosowany, było to wystarczające, ponieważ podałeś płytki, prosty przykład, a także bardziej złożoną funkcję głęboką.
Seiyria

2
@Seiyria hejterzy będą nienawidzić, tak myślę ... Zrobiłem oba, ponieważ początkowo myślałem, omitże będzie to głęboka różnica, ale się myliłem, więc uwzględniono również dla porównania.
drzaus

1
Niezłe rozwiązanie. Proponuję zmienić r[k] = ... : vw r[k] = ... : {'a':v, 'b':b[k] }, w ten sposób można zobaczyć dwie wartości.
guyaloni,

2
Obie te zwracają fałszywie ujemny, gdy obiekty są identyczne, ale w przeciwnym razie druga ma więcej elementów, np {a:1, b:2}i {a:1, b:2, c:3}.
JJJ

1
Powinno być _.omitByzamiast _.omit.
JP

48

Chciałbym zaoferować rozwiązanie ES6 ... Jest to różnicowanie jednokierunkowe, co oznacza, że ​​zwróci klucze / wartości o2, które nie są identyczne z ich odpowiednikami w 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
Fajne rozwiązanie, ale możesz chcieć sprawdzić tę if(o1[key] === o1[key])linię koleś
bm_i

Czy kod jest kompletny? DostajęUncaught SyntaxError: Unexpected token ...
Seano

2
Podoba mi się rozwiązanie, ale ma jeden problem, jeśli obiekt jest głębszy niż jeden poziom, zwróci wszystkie wartości w zmienionych zagnieżdżonych obiektach - a przynajmniej tak się dzieje.
Spurious

3
Tak, to nie jest rekurencyjne @Spurious
Nemesarial

2
Pamiętaj tylko, że dzięki temu rozwiązaniu dla każdego elementu w obiekcie otrzymujesz całkowicie nowy obiekt zbudowany ze wszystkimi istniejącymi elementami skopiowanymi do niego, aby dodać jeden element do tablicy. W przypadku małych obiektów jest w porządku, ale w przypadku większych obiektów będzie zwalniał wykładniczo.
Malvineous

22

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

Nie używam klucza / obiektu / źródła, ale zostawiłem go tam, jeśli chcesz uzyskać do nich dostęp. Porównanie obiektów po prostu uniemożliwia konsoli wydrukowanie różnic w konsoli od elementu najbardziej zewnętrznego do elementu najbardziej wewnętrznego.

Możesz dodać trochę logiki do obsługi tablic. Być może najpierw posortuj tablice. To bardzo elastyczne rozwiązanie.

EDYTOWAĆ

Zmieniono z _.merge na _.mergeWith z powodu aktualizacji lodash. Dziękujemy Aviron za zauważenie zmiany.


6
W lodash 4.15.0 _.merge z funkcją dostosowywania nie jest już obsługiwany, więc zamiast tego należy użyć _.mergeWith.
Aviran Cohen

1
ta funkcja jest świetna, ale nie działa w zagnieżdżonym obiekcie.
Joe Allen

13

Oto biblioteka JavaScript, której można użyć do znalezienia różnic między dwoma obiektami JavaScript:

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

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

Możesz używać biblioteki rekursywno-różnicowej w przeglądarce, a także w Node.js. W przypadku przeglądarki wykonaj następujące czynności:

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

Natomiast w node.js możesz wymagać modułu „recursive-diff” i używać go jak poniżej:

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

Nie uwzględni to na przykład zmian właściwości Data.
trollkotze

Dodano obsługę daty
Anant

9

Obecnie dostępnych jest do tego sporo modułów. Niedawno napisałem moduł, aby to zrobić, ponieważ nie byłem zadowolony z wielu różnych modułów, które znalazłem. To się nazywa odiff: https://github.com/Tixit/odiff . Wymieniłem też kilka najpopularniejszych modułów i dlaczego nie były one akceptowane w odiffpliku Readme , które można przejrzeć, jeśli odiffnie mają pożądanych właściwości. Oto przykład:

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

Istnieje moduł npm z ponad 500 000 pobrań tygodniowo: https://www.npmjs.com/package/deep-object-diff

Podoba mi się obiekt jak reprezentacja różnic - szczególnie, gdy łatwo jest zobaczyć strukturę, kiedy jest sformatowana.

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

Użyłem tego fragmentu kodu do wykonania opisanego zadania:

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

dostaniesz nowy obiekt, który połączy wszystkie zmiany między starym obiektem a nowym obiektem z twojego formularza


1
Używam tutaj frameworka Ext, ale możesz go zastąpić i użyć
dowolnego

Scalanie obiektów jest banalne i można to zrobić tak łatwo, jak $.extend(true,obj1,obj2)przy użyciu jQuery. To wcale nie jest to, czego potrzebuję. Potrzebuję różnicy między dwoma przedmiotami, a nie ich kombinacji.
Martin Jespersen

to wspaniałe, że stosuje się tutaj Ext
nadtlenek

2

Opracowałem funkcję o nazwie „CompareValue ()” w JavaScript. zwraca, czy wartość jest taka sama, czy nie. Wywołałem funkcję CompareValue () w pętli for jednego obiektu. możesz uzyskać różnicę dwóch obiektów w 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);


1

Wiem, że spóźniłem się na przyjęcie, ale potrzebowałem czegoś podobnego, co powyższe odpowiedzi nie pomogły.

Korzystałem z funkcji zegarka $ Angulara do wykrywania zmian w zmiennej. Nie tylko musiałem wiedzieć, czy właściwość zmieniła się w zmiennej, ale chciałem również upewnić się, że zmieniona właściwość nie była tymczasowym polem obliczonym. Innymi słowy, chciałem zignorować pewne właściwości.

Oto kod: https://jsfiddle.net/rv01x6jo/

Oto jak go użyć:

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

Mam nadzieję, że to komuś pomoże.


Podaj również kod w swojej odpowiedzi, a nie tylko skrzypce.
xpy

Wygląda na to, że definProperty rozwiązałoby ten problem z lepszą wydajnością, jeśli dobrze pamiętam, działa aż do IE9.
Piotra

Dzięki..!! Twój kod działa jak urok i uratował mi dzień. Mam obiekt json z 1250 liniami i daje mi to dokładnie o / p, czego chcę.
Tejas Mehta

1

Po prostu używam ramdy, aby rozwiązać ten sam problem, muszę wiedzieć, co zostało zmienione w nowym obiekcie. Oto mój projekt.

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)

wynikiem jest nazwa właściwości i jej status.

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

1

Oto wersja maszynopisu kodu @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;

  }
}

1

Oto zmodyfikowana wersja czegoś znalezionego na 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;
        }
    });
}

1

Zmodyfikowałem odpowiedź @ sbgoran, aby wynikowy obiekt diff zawierał tylko zmienione wartości i pomijał te same wartości. Ponadto pokazuje zarówno wartość oryginalną, jak i zaktualizowaną wartość .

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

Napisałem już funkcję dla jednego z moich projektów, która porówna obiekt jako opcję użytkownika z jego wewnętrznym klonem. Może również sprawdzać, a nawet zastępować wartości domyślne, jeśli użytkownik wprowadził zły typ danych lub usunął go w czystym javascript.

W IE8 działa w 100%. Testowany pomyślnie.

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

/ * wynik

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

Bardziej rozbudowana i uproszczona funkcja z odpowiedzi sbgorana.
Pozwala to na głębokie skanowanie i znalezienie podobieństwa tablicy.

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

Natknąłem się tutaj, próbując znaleźć sposób na uzyskanie różnicy między dwoma przedmiotami. To jest moje rozwiązanie przy użyciu 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

Fragment kodu poniżej:

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

Przyjąłem powyższą odpowiedź @sbgoran i zmodyfikowałem ją w moim przypadku tak samo, jak potrzebne pytanie, aby traktować tablice jako zestawy (tzn. Kolejność nie jest ważna dla 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);
  }
 };
}();

0

Oto rozwiązanie, które:

  • Maszynopis (ale łatwo przekonwertowany na JavaScript)
  • nie mają zależności lib
  • ogólny i nie przejmuje się sprawdzaniem typów obiektów (poza objecttypem)
  • obsługuje właściwości o wartości undefined
  • deep of not (domyślnie)

Najpierw definiujemy interfejs wyniku porównania:

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

ze szczególnym przypadkiem zmiany, w którym chcemy wiedzieć, jakie są stare i nowe wartości:

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

Następnie możemy zapewnić difffunkcję, która jest zaledwie dwiema pętlami (z rekurencyjnością, jeśli deepjest 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';
  }
}

Na przykład wywołanie:

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

wróci:

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

i wywołanie tego samego z deeptrzecim parametrem zwróci:

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.