Jak przeprowadzić dokładne porównanie 2 obiektów z lodash?

309

Mam 2 zagnieżdżone obiekty, które są różne i muszę wiedzieć, czy różnią się jedną z ich zagnieżdżonych właściwości.

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

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

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

Obiekt może być znacznie bardziej złożony z większą liczbą zagnieżdżonych właściwości. Ale ten jest dobrym przykładem. Mam opcję korzystania z funkcji rekurencyjnych lub czegoś podobnego do lodash ...

JLavoie
źródło
Do głębokiego porównania stackoverflow.com/a/46003894/696535
Paweł
7
_.isEqual(value, other)Przeprowadza dokładne porównanie dwóch wartości, aby ustalić, czy są one równoważne. lodash.com/docs#isEqual
Lukas Liesis
JSON.stringify ()
xgqfrms
10
JSON.stringify () jest niepoprawny: JSON.stringify ({a: 1, b: 2})! == JSON.stringify ({b: 2, a: 1})
Shl

Odpowiedzi:

475

Łatwym i eleganckim rozwiązaniem jest użycie _.isEqual, które umożliwia dokładne porównanie:

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

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

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

_.isEqual(a, b); // returns false if different

Jednak to rozwiązanie nie pokazuje, która właściwość jest inna.

http://jsfiddle.net/bdkeyn0h/

JLavoie
źródło
2
Wiem, że odpowiedź jest dość stara, ale chcę dodać, że _.isEqualmoże być dość trudna. Jeśli skopiujesz obiekt i zmienisz tam niektóre wartości, nadal będzie to prawda, ponieważ odwołanie jest takie samo. Dlatego należy zachować ostrożność podczas korzystania z tej funkcji.
oruckdeschel,
23
@oruckdeschel, jeśli odwołanie jest takie samo, to ten sam obiekt. stąd jest równy. ten wskaźnik jest trudny, a nie lodash. lodash jest niesamowity.
facet mograbi
265

Jeśli chcesz wiedzieć, które właściwości są różne, użyj funkcji zmniejsz () :

_.reduce(a, function(result, value, key) {
    return _.isEqual(value, b[key]) ?
        result : result.concat(key);
}, []);
// → [ "prop2" ]
Adam Boduch
źródło
36
Zauważ, że spowoduje to wyświetlenie tylko innych właściwości pierwszego poziomu. (Więc to nie jest tak naprawdę głębokie w sensie generowania właściwości, które są różne.)
Bloke
16
Nie spowoduje to również pobrania właściwości wb, które nie są w a.
Ed Staub,
3
i _.reduce(a, (result, value, key) => _.isEqual(value, b[key]) ? result : result.concat(key), [])dla
jednowierszowego
1
Wersja zawierająca klucz: wartośćlet edited = _.reduce(a, function(result, value, key) { return _.isEqual(value, b[key]) ? result : result.concat( { [key]: value } ); }, []);
Aline Matos
47

Dla każdego, kto natknie się na ten wątek, oto bardziej kompletne rozwiązanie. Porówna dwa obiekty i poda klucz wszystkich właściwości, które są albo tylko w obiekcie object1 , tylko w obiekcie object2 , albo obaw obiektach object1 i object2, ale mają różne wartości :

/*
 * Compare two objects by reducing an array of keys in obj1, having the
 * keys in obj2 as the intial value of the result. Key points:
 *
 * - All keys of obj2 are initially in the result.
 *
 * - If the loop finds a key (from obj1, remember) not in obj2, it adds
 *   it to the result.
 *
 * - If the loop finds a key that are both in obj1 and obj2, it compares
 *   the value. If it's the same value, the key is removed from the result.
 */
function getObjectDiff(obj1, obj2) {
    const diff = Object.keys(obj1).reduce((result, key) => {
        if (!obj2.hasOwnProperty(key)) {
            result.push(key);
        } else if (_.isEqual(obj1[key], obj2[key])) {
            const resultKeyIndex = result.indexOf(key);
            result.splice(resultKeyIndex, 1);
        }
        return result;
    }, Object.keys(obj2));

    return diff;
}

Oto przykładowy wynik:

// Test
let obj1 = {
    a: 1,
    b: 2,
    c: { foo: 1, bar: 2},
    d: { baz: 1, bat: 2 }
}

let obj2 = {
    b: 2, 
    c: { foo: 1, bar: 'monkey'}, 
    d: { baz: 1, bat: 2 }
    e: 1
}
getObjectDiff(obj1, obj2)
// ["c", "e", "a"]

Jeśli nie zależy Ci na zagnieżdżonych obiektach i chcesz pominąć lodash, możesz zastąpić _.isEqualporównanie wartości normalnej, np obj1[key] === obj2[key].

Johan Persson
źródło
Ta wybrana odpowiedź jest poprawna tylko do testowania równości. Jeśli chcesz wiedzieć, jakie są różnice, nie ma oczywistego sposobu, aby je wymienić, ale ta odpowiedź jest całkiem dobra, podając tylko listę kluczy właściwości najwyższego poziomu, gdzie jest różnica. (I daje odpowiedź jako funkcję, co czyni ją użyteczną.)
Sigfried
Jaka jest różnica między robieniem tego a używaniem _.isEqual (obj1, obj2)? Co robi dodanie czeku hasOwnProperty, że nie robi to _.isEqual? Przyjąłem założenie, że jeśli obiekt obj1 miałby właściwość, której obiekt obj2 nie miał, _.isEqual nie zwróciłoby wartości true?
Jaked222,
2
@ Jaked222 - różnica polega na tym, że isEqual zwraca wartość logiczną mówiącą, czy obiekty są równe, czy nie, podczas gdy powyższa funkcja mówi, co różni się między dwoma obiektami (jeśli są różne). Jeśli interesuje Cię tylko to, czy dwa obiekty są takie same, isEqual wystarczy. Jednak w wielu przypadkach chcesz wiedzieć, jaka jest różnica między dwoma obiektami. Przykładem może być wykrycie zmian przed czymś i po nim, a następnie wywołanie zdarzenia na podstawie zmian.
Johan Persson,
30

Na podstawie odpowiedzi Adama Boducha napisałem tę funkcję, która porównuje dwa obiekty w najgłębszym możliwym sensie , zwracając ścieżki o różnych wartościach, a także ścieżki, których brakuje w jednym lub drugim obiekcie.

Kod nie został napisany z myślą o wydajności, a ulepszenia w tym zakresie są mile widziane, ale oto podstawowa forma:

var compare = function (a, b) {

  var result = {
    different: [],
    missing_from_first: [],
    missing_from_second: []
  };

  _.reduce(a, function (result, value, key) {
    if (b.hasOwnProperty(key)) {
      if (_.isEqual(value, b[key])) {
        return result;
      } else {
        if (typeof (a[key]) != typeof ({}) || typeof (b[key]) != typeof ({})) {
          //dead end.
          result.different.push(key);
          return result;
        } else {
          var deeper = compare(a[key], b[key]);
          result.different = result.different.concat(_.map(deeper.different, (sub_path) => {
            return key + "." + sub_path;
          }));

          result.missing_from_second = result.missing_from_second.concat(_.map(deeper.missing_from_second, (sub_path) => {
            return key + "." + sub_path;
          }));

          result.missing_from_first = result.missing_from_first.concat(_.map(deeper.missing_from_first, (sub_path) => {
            return key + "." + sub_path;
          }));
          return result;
        }
      }
    } else {
      result.missing_from_second.push(key);
      return result;
    }
  }, result);

  _.reduce(b, function (result, value, key) {
    if (a.hasOwnProperty(key)) {
      return result;
    } else {
      result.missing_from_first.push(key);
      return result;
    }
  }, result);

  return result;
}

Możesz wypróbować kod za pomocą tego fragmentu kodu (zalecane jest uruchomienie w trybie pełnej strony):

Rashad Saleh
źródło
4
Właśnie naprawiłem błąd, ale aby cię powiadomić, powinieneś sprawdzić istnienie klucza w obiekcie b za pomocą b.hasOwnProperty(key)lubkey in b nie b[key] != undefined. W poprzedniej używanej wersji b[key] != undefinedfunkcja zwróciła niepoprawną różnicę dla obiektów zawierających undefined, jak w compare({disabled: undefined}, {disabled: undefined}). W rzeczywistości stara wersja również miała problemy null; możesz uniknąć takich problemów, zawsze używając ===i!== zamiast ==i !=.
Rory O'Kane
23

Oto zwięzłe rozwiązanie:

_.differenceWith(a, b, _.isEqual);
DPG
źródło
7
Wydaje mi się, że nie działa z obiektami. Zamiast tego zwraca pustą tablicę.
tomhughes
2
Również pusta tablica z Lodash 4.17.4
aristidesfl
@ Z. Khullah Jeśli zadziałało w ten sposób, nie jest udokumentowane.
Brendon
1
@Brendon, @THughes, @aristidesfl przepraszam, pomieszałem rzeczy, działa z tablicami obiektów, ale nie do głębokich porównań obiektów. Jak się okazuje, jeśli żaden z parametrów nie jest tablicami, lodash po prostu wróci [].
Z. Khullah
7

Aby rekurencyjnie pokazać, jak obiekt różni się od innych, możesz użyć _.reduce w połączeniu z _.isEqual i _.isPlainObject . W takim przypadku możesz porównać, jak a różni się od b lub jak b różni się od a:

var a = {prop1: {prop1_1: 'text 1', prop1_2: 'text 2', prop1_3: [1, 2, 3]}, prop2: 2, prop3: 3};
var b = {prop1: {prop1_1: 'text 1', prop1_3: [1, 2]}, prop2: 2, prop3: 4};

var diff = function(obj1, obj2) {
  return _.reduce(obj1, function(result, value, key) {
    if (_.isPlainObject(value)) {
      result[key] = diff(value, obj2[key]);
    } else if (!_.isEqual(value, obj2[key])) {
      result[key] = value;
    }
    return result;
  }, {});
};

var res1 = diff(a, b);
var res2 = diff(b, a);
console.log(res1);
console.log(res2);
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>

Santiago Bendavid
źródło
7

Prosta _.isEqualmetoda użycia , będzie działać dla wszystkich porównujących ...

  • Uwaga: Ta metoda obsługuje porównywanie tablic, buforów tablic, boolanów, * obiektów daty, obiektów błędów, map, liczb, Objectobiektów, wyrażeń regularnych, * zbiorów, ciągów, symboli i tablic maszynowych. Objectobiekty są porównywane * według własnych, nie odziedziczonych, wyliczalnych właściwości. Funkcje i węzły DOM * nie są obsługiwane.

Więc jeśli masz poniżej:

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

Jeśli tak: _.isEqual(firstName, otherName);,

zwróci prawdę

I jeśli const fullName = {firstName: "Alireza", familyName: "Dezfoolian"};

Jeśli tak: _.isEqual(firstName, fullName);,

zwróci fałsz

Alireza
źródło
6

Ten kod zwraca obiekt ze wszystkimi właściwościami, które mają inną wartość, a także wartości obu obiektów. Przydatne do rejestrowania różnicy.

var allkeys = _.union(_.keys(obj1), _.keys(obj2));
var difference = _.reduce(allkeys, function (result, key) {
  if ( !_.isEqual(obj1[key], obj2[key]) ) {
    result[key] = {obj1: obj1[key], obj2: obj2[key]}
  }
  return result;
}, {});
blikblum
źródło
3

Bez użycia lodash / podkreślenia napisałem ten kod i działa dobrze dla mnie do głębokiego porównania object1 z object2

function getObjectDiff(a, b) {
    var diffObj = {};
    if (Array.isArray(a)) {
        a.forEach(function(elem, index) {
            if (!Array.isArray(diffObj)) {
                diffObj = [];
            }
            diffObj[index] = getObjectDiff(elem, (b || [])[index]);
        });
    } else if (a != null && typeof a == 'object') {
        Object.keys(a).forEach(function(key) {
            if (Array.isArray(a[key])) {
                var arr = getObjectDiff(a[key], b[key]);
                if (!Array.isArray(arr)) {
                    arr = [];
                }
                arr.forEach(function(elem, index) {
                    if (!Array.isArray(diffObj[key])) {
                        diffObj[key] = [];
                    }
                    diffObj[key][index] = elem;
                });
            } else if (typeof a[key] == 'object') {
                diffObj[key] = getObjectDiff(a[key], b[key]);
            } else if (a[key] != (b || {})[key]) {
                diffObj[key] = a[key];
            } else if (a[key] == (b || {})[key]) {
                delete a[key];
            }
        });
    }
    Object.keys(diffObj).forEach(function(key) {
        if (typeof diffObj[key] == 'object' && JSON.stringify(diffObj[key]) == '{}') {
            delete diffObj[key];
        }
    });
    return diffObj;
}
Sridhar Gudimela
źródło
3

Dokładne porównanie przy użyciu szablonu (zagnieżdżonych) właściwości do sprawdzenia

function objetcsDeepEqualByTemplate(objectA, objectB, comparisonTemplate) {
  if (!objectA || !objectB) return false

  let areDifferent = false
  Object.keys(comparisonTemplate).some((key) => {
    if (typeof comparisonTemplate[key] === 'object') {
      areDifferent = !objetcsDeepEqualByTemplate(objectA[key], objectB[key], comparisonTemplate[key])
      return areDifferent
    } else if (comparisonTemplate[key] === true) {
      areDifferent = objectA[key] !== objectB[key]
      return areDifferent
    } else {
      return false
    }
  })

  return !areDifferent
}

const objA = { 
  a: 1,
  b: {
    a: 21,
    b: 22,
  },
  c: 3,
}

const objB = { 
  a: 1,
  b: {
    a: 21,
    b: 25,
  },
  c: true,
}

// template tells which props to compare
const comparisonTemplateA = {
  a: true,
  b: {
    a: true
  }
}
objetcsDeepEqualByTemplate(objA, objB, comparisonTemplateA)
// returns true

const comparisonTemplateB = {
  a: true,
  c: true
}
// returns false
objetcsDeepEqualByTemplate(objA, objB, comparisonTemplateB)

To zadziała w konsoli. W razie potrzeby można dodać obsługę tablicy

Paweł
źródło
2

Wzięłam dźgnięcie kodem Adama Boducha, aby wyprowadzić głęboki plik różnicowy - jest to całkowicie niesprawdzone, ale fragmenty są:

function diff (obj1, obj2, path) {
    obj1 = obj1 || {};
    obj2 = obj2 || {};

    return _.reduce(obj1, function(result, value, key) {
        var p = path ? path + '.' + key : key;
        if (_.isObject(value)) {
            var d = diff(value, obj2[key], p);
            return d.length ? result.concat(d) : result;
        }
        return _.isEqual(value, obj2[key]) ? result : result.concat(p);
    }, []);
}

diff({ foo: 'lol', bar: { baz: true }}, {}) // returns ["foo", "bar.baz"]
Deminetix
źródło
1
Działa jak urok, tylko że kolejność obj1 i obj2 jest ważna. Na przykład: diff({}, { foo: 'lol', bar: { baz: true }}) // returns []
amangpt777
2

Jak zostało zadane, oto funkcja porównywania obiektów rekurencyjnych. I trochę więcej. Zakładając, że podstawowym zastosowaniem takiej funkcji jest kontrola obiektów, mam coś do powiedzenia. Całkowite dokładne porównanie jest złym pomysłem, gdy pewne różnice są nieistotne. Na przykład ślepe głębokie porównanie w stwierdzeniach TDD sprawia, że ​​testy stają się niepotrzebne. Z tego powodu chciałbym wprowadzić o wiele bardziej wartościowy różnicowy fragment . Jest to rekurencyjny odpowiednik poprzedniego wkładu do tego wątku. Ignoruje klucze nie występujące w A

var bdiff = (a, b) =>
    _.reduce(a, (res, val, key) =>
        res.concat((_.isPlainObject(val) || _.isArray(val)) && b
            ? bdiff(val, b[key]).map(x => key + '.' + x) 
            : (!b || val != b[key] ? [key] : [])),
        []);

BDiff umożliwia sprawdzanie oczekiwanych wartości przy jednoczesnym tolerowaniu innych właściwości, co jest dokładnie tym, czego chcesz do automatycznej kontroli. Pozwala to na budowanie wszelkiego rodzaju zaawansowanych twierdzeń. Na przykład:

var diff = bdiff(expected, actual);
// all expected properties match
console.assert(diff.length == 0, "Objects differ", diff, expected, actual);
// controlled inequality
console.assert(diff.length < 3, "Too many differences", diff, expected, actual);

Wracając do kompletnego rozwiązania. Budowanie pełnego tradycyjnego diff z bdiff jest banalne:

function diff(a, b) {
    var u = bdiff(a, b), v = bdiff(b, a);
    return u.filter(x=>!v.includes(x)).map(x=>' < ' + x)
    .concat(u.filter(x=>v.includes(x)).map(x=>' | ' + x))
    .concat(v.filter(x=>!u.includes(x)).map(x=>' > ' + x));
};

Uruchomienie powyższej funkcji na dwóch złożonych obiektach spowoduje wyświetlenie czegoś podobnego do tego:

 [
  " < components.0.components.1.components.1.isNew",
  " < components.0.cryptoKey",
  " | components.0.components.2.components.2.components.2.FFT.min",
  " | components.0.components.2.components.2.components.2.FFT.max",
  " > components.0.components.1.components.1.merkleTree",
  " > components.0.components.2.components.2.components.2.merkleTree",
  " > components.0.components.3.FFTResult"
 ]

Na koniec, aby rzucić okiem na różnice między wartościami, możemy chcieć bezpośrednio ewaluować () wyjście diff. W tym celu potrzebujemy brzydszej wersji bdiff, która wyświetla poprawne składniowo ścieżki:

// provides syntactically correct output
var bdiff = (a, b) =>
    _.reduce(a, (res, val, key) =>
        res.concat((_.isPlainObject(val) || _.isArray(val)) && b
            ? bdiff(val, b[key]).map(x => 
                key + (key.trim ? '':']') + (x.search(/^\d/)? '.':'[') + x)
            : (!b || val != b[key] ? [key + (key.trim ? '':']')] : [])),
        []);

// now we can eval output of the diff fuction that we left unchanged
diff(a, b).filter(x=>x[1] == '|').map(x=>[x].concat([a, b].map(y=>((z) =>eval('z.' + x.substr(3))).call(this, y)))));

To da wynik podobny do tego:

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

Licencja MIT;)

użytkownik46748
źródło
1

Uzupełniając odpowiedź Adama Boducha, ta uwzględnia różnice we właściwościach

const differenceOfKeys = (...objects) =>
  _.difference(...objects.map(obj => Object.keys(obj)));
const differenceObj = (a, b) => 
  _.reduce(a, (result, value, key) => (
    _.isEqual(value, b[key]) ? result : [...result, key]
  ), differenceOfKeys(b, a));
Ricardo Freitas
źródło
1

Jeśli potrzebujesz tylko klucza porównania:

 _.reduce(a, function(result, value, key) {
     return b[key] === undefined ? key : []
  }, []);
Giuseppe
źródło
0

Oto prosty maszynopis z funkcją sprawdzania głębokości różnic Lodash, która utworzy nowy obiekt z różnicami między starym a nowym obiektem.

Na przykład, gdybyśmy mieli:

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

wynikowy obiekt byłby:

const result: {b: 3};

Jest również kompatybilny z wielopoziomowymi głębokimi obiektami, w przypadku tablic może wymagać drobnych poprawek.

import * as _ from "lodash";

export const objectDeepDiff = (data: object | any, oldData: object | any) => {
  const record: any = {};
  Object.keys(data).forEach((key: string) => {
    // Checks that isn't an object and isn't equal
    if (!(typeof data[key] === "object" && _.isEqual(data[key], oldData[key]))) {
      record[key] = data[key];
    }
    // If is an object, and the object isn't equal
    if ((typeof data[key] === "object" && !_.isEqual(data[key], oldData[key]))) {
      record[key] = objectDeepDiff(data[key], oldData[key]);
    }
  });
  return record;
};
jmoore255
źródło
-1
var isEqual = function(f,s) {
  if (f === s) return true;

  if (Array.isArray(f)&&Array.isArray(s)) {
    return isEqual(f.sort(), s.sort());
  }
  if (_.isObject(f)) {
    return isEqual(f, s);
  }
  return _.isEqual(f, s);
};
Krzyżowiec
źródło
To jest nieprawidłowe Nie możesz ===bezpośrednio porównywać obiektów , { a: 20 } === { a: 20 }zwróci false, ponieważ porównuje prototyp. Bardziej właściwym sposobem przede wszystkim porównywania obiektów jest zawijanie ichJSON.stringify()
Herrgott,
if (f === s) zwraca true; - służy tylko do rekurencji. Tak a: 20} === {a: 20} zwróci false i przejdzie do następnego warunku
Crusader
dlaczego nie tylko _.isEqual(f, s)? :)
Herrgott,
Spowoduje to nieskończoną pętlę rekurencyjną, ponieważ jeśli fjest obiektem i dojdziesz do if (_.isObject(f))siebie, po prostu wróć przez funkcję i ponownie uderz w ten punkt. To samo dotyczyf (Array.isArray(f)&&Array.isArray(s))
rady
-2

było to oparte na @JLavoie , przy użyciu lodash

let differences = function (newObj, oldObj) {
      return _.reduce(newObj, function (result, value, key) {
        if (!_.isEqual(value, oldObj[key])) {
          if (_.isArray(value)) {
            result[key] = []
            _.forEach(value, function (innerObjFrom1, index) {
              if (_.isNil(oldObj[key][index])) {
                result[key].push(innerObjFrom1)
              } else {
                let changes = differences(innerObjFrom1, oldObj[key][index])
                if (!_.isEmpty(changes)) {
                  result[key].push(changes)
                }
              }
            })
          } else if (_.isObject(value)) {
            result[key] = differences(value, oldObj[key])
          } else {
            result[key] = value
          }
        }
        return result
      }, {})
    }

https://jsfiddle.net/EmilianoBarboza/0g0sn3b9/8/

Emiliano Barboza
źródło
-2

tylko używając waniliowego js

let a = {};
let b = {};

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

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

JSON.stringify(a) === JSON.stringify(b);
// false
b.prop2 = { prop3: 2};

JSON.stringify(a) === JSON.stringify(b);
// true

wprowadź opis zdjęcia tutaj

xgqfrms
źródło
1
Ta metoda nie powiedziałaby, które atrybuty są różne.
JLavoie,
2
W takim przypadku kolejność atrybutów wpływa na wynik.
Victor Oliveira
-2

Aby skorzystać z odpowiedzi Sridhara Gudimeli , tutaj jest ona aktualizowana w sposób, który uszczęśliwia Flow:

"use strict"; /* @flow */



//  E X P O R T

export const objectCompare = (objectA: any, objectB: any) => {
  let diffObj = {};

  switch(true) {
    case (Array.isArray(objectA)):
      objectA.forEach((elem, index) => {
        if (!Array.isArray(diffObj))
          diffObj = [];

        diffObj[index] = objectCompare(elem, (objectB || [])[index]);
      });

      break;

    case (objectA !== null && typeof objectA === "object"):
      Object.keys(objectA).forEach((key: any) => {
        if (Array.isArray(objectA[key])) {
          let arr = objectCompare(objectA[key], objectB[key]);

          if (!Array.isArray(arr))
            arr = [];

          arr.forEach((elem, index) => {
            if (!Array.isArray(diffObj[key]))
              diffObj[key] = [];

            diffObj[key][index] = elem;
          });
        } else if (typeof objectA[key] === "object")
          diffObj[key] = objectCompare(objectA[key], objectB[key]);
        else if (objectA[key] !== (objectB || {})[key])
          diffObj[key] = objectA[key];
        else if (objectA[key] === (objectB || {})[key])
          delete objectA[key];
      });

      break;

    default:
      break;
  }

  Object.keys(diffObj).forEach((key: any) => {
    if (typeof diffObj[key] === "object" && JSON.stringify(diffObj[key]) === "{}")
      delete diffObj[key];
  });

  return diffObj;
};
NetOperator Wibby
źródło