Як зробити глибоке порівняння двох об'єктів з лодашем?


309

У мене є два вкладені об'єкти, які відрізняються, і мені потрібно знати, чи мають вони різницю в одному зі своїх вкладених властивостей.

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

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

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

Об'єкт може бути набагато складнішим з більш вкладеними властивостями. Але це хороший приклад. У мене є можливість використовувати рекурсивні функції або щось із lodash ...


Для глибокого порівняння stackoverflow.com/a/46003894/696535
Pawel,

7
_.isEqual(value, other)Виконує глибоке порівняння двох значень, щоб визначити, чи вони еквівалентні. lodash.com/docs#isEqual
Лукас Ліесіс

JSON.stringify ()
xgqfrms

10
JSON.stringify () помиляється: JSON.stringify ({a: 1, b: 2})! == JSON.stringify ({b: 2, a: 1})
Шл

Відповіді:


475

Просте та елегантне рішення - це використання _.isEqual, яке забезпечує глибоке порівняння:

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

Однак це рішення не показує, яка властивість відрізняється.

http://jsfiddle.net/bdkeyn0h/


2
Я знаю, що відповідь досить стара, але хочу додати, що _.isEqualможе бути досить хитрою. Якщо ви скопіюєте об'єкт і зміните там деякі значення, він все одно відображатиметься правдою, оскільки посилання є однаковим. Тож слід бути обережними, використовуючи цю функцію.
oruckdeschel

23
@oruckdeschel, якщо посилання однаковий, це той самий об'єкт. значить, вона дорівнює. це вказівник, який хитрий не судаш. лодаш - приголомшливий.
хлопець mograbi

265

Якщо вам потрібно знати, які властивості відрізняються, використовуйте зменшити () :

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

36
Зверніть увагу, що це виведе лише перші властивості різних рівнів. (Отже, це не дуже глибоко в сенсі виведення властивостей, які відрізняються.)
Блокувати

16
Крім того, це не вибере властивості в b, які не знаходяться в a.
Ед Штауб

3
а _.reduce(a, (result, value, key) => _.isEqual(value, b[key]) ? result : result.concat(key), [])для однолінійного рішення
ES6

1
Версія, що підкреслює ключ: valuelet edited = _.reduce(a, function(result, value, key) { return _.isEqual(value, b[key]) ? result : result.concat( { [key]: value } ); }, []);
Aline Matos

47

Для тих, хто натрапляє на цю нитку, ось більш повне рішення. Він порівняє два об'єкти та надасть вам ключ усіх властивостей, які є або лише в object1 , тільки в object2 , або обидва є в object1 та object2, але мають різні значення :

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

Ось приклад результату:

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

Якщо вам не байдуже вкладені об'єкти і хочете пропустити квартир, ви можете замінити _.isEqualпорівняння на звичайне значення, наприклад obj1[key] === obj2[key].


Ця обрана відповідь правильна для просто перевірки рівності. Якщо вам потрібно знати, в чому полягають відмінності, очевидного способу їх перерахування немає, але ця відповідь є досить хорошою, просто даючи список ключових властивостей верхнього рівня, де є різниця. (І він дає відповідь як функцію, яка робить його корисним.)
Зігфрід

Яка різниця між цим і просто використанням _.isEqual (obj1, obj2)? Що додає чек на hasOwnProperty, що _.isEqual не робить? Я був припущений, що якби у obj1 було властивість, якої не було у obj2, _.isEqual не повертав би істину ..?
Jaked222

2
@ Jaked222 - відмінність полягає в тому, що isEqual повертає булеву інформацію про те, чи об'єкти рівні чи ні, тоді як функція, що знаходиться вище, говорить про те, що відрізняється між двома об'єктами (якщо вони різні). Якщо вас цікавить лише те, чи два об'єкти однакові, isEqual цілком достатньо. Однак у багатьох випадках ви хочете знати, у чому різниця між двома об'єктами. Прикладом може бути, якщо ви хочете виявити зміни до і після чогось, а потім відправити подію на основі змін.
Йохан Перссон

30

Виходячи з відповіді Адама Бодуха , я написав цю функцію, яка порівнює два об’єкти в найглибшому можливому сенсі , повертаючи шляхи, що мають різні значення, а також шляхи, відсутні у одного чи іншого об'єкта.

Код не був написаний з урахуванням ефективності, і вдосконалення в цьому плані найкраще, але ось основна форма:

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

Ви можете спробувати код за допомогою цього фрагмента (рекомендується запуск у повному режимі сторінки):


4
Я просто виправив помилку, але, щоб повідомити вам, ви повинні перевірити існування ключа в об'єкті b за допомогою b.hasOwnProperty(key)абоkey in b , а не з b[key] != undefined. Зі старою версією, яка використовувалась b[key] != undefined, функція повертала неправильну різницю для об'єктів, що містять undefined, як у compare({disabled: undefined}, {disabled: undefined}). Насправді, стара версія також мала проблеми з null; Ви можете уникнути подібних проблем, завжди використовуючи ===та!== замість ==і !=.
Rory O'Kane

23

Ось стисле рішення:

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

7
Здається, це не працює для мене з предметами. Натомість повертає порожній масив.
tomhughes

2
Також отримує порожній масив з Лодашем 4.17.4
aristidesfl

@ Z.Khullah Якщо це діяло таким чином, це не документально підтверджено.
Брендон

1
@Brendon, @THughes, @aristidesfl Вибачте, я змішав речі, він працює з масивами об'єктів, але не для глибоких порівнянь об'єктів. Як виявляється, якщо жоден параметр не є масивами, lodash просто повернеться [].
З. Хулла

7

Щоб рекурсивно показати, чим об’єкт відрізняється від інших, ви можете використовувати _.reduce у поєднанні з _.isEqual та _.isPlainObject . У цьому випадку ви можете порівняти, як a відрізняється від b або як b відрізняється від a:

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

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

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


7

Простий _.isEqualметод використання , він буде працювати для всіх порівнянь ...

  • Примітка: Цей метод підтримує порівняння масивів, буферів масивів, булевих даних, * об’єктів дати, об'єктів помилок, карт, чисел, Objectоб'єктів, регулярних виразів, * наборів, рядків, символів та набраних масивів. Objectоб’єкти порівнюються * за власними, не успадкованими, перелічуваними властивостями. Функції та вузли DOM * не підтримуються.

Тож якщо у вас є нижче:

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

Якщо ти зробиш: _.isEqual(firstName, otherName); ,

воно повернеться правдою

І якщо const fullName = {firstName: "Alireza", familyName: "Dezfoolian"};

Якщо ти зробиш: _.isEqual(firstName, fullName); ,

повернеться помилковим


6

Цей код повертає об'єкт з усіма властивостями, які мають різне значення, а також значення обох об'єктів. Корисно для реєстрації різниці.

var allkeys = _.union(_.keys(obj1), _.keys(obj2));
var difference = _.reduce(allkeys, function (result, key) {
  if ( !_.isEqual(obj1[key], obj2[key]) ) {
    result[key] = {obj1: obj1[key], obj2: obj2[key]}
  }
  return result;
}, {});

3

Без використання lodash / підкреслення, я написав цей код і добре працює для мене для глибокого порівняння object1 з object2

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

3

Глибоке порівняння за допомогою шаблону (вкладених) властивостей для перевірки

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)

Це буде працювати в консолі. Підтримка масиву може бути додана за потреби


2

Я взяв кодовий код Адама Бодуха, щоб вивести глибокий розбіг - це абсолютно не перевірено, але фрагменти є:

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

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

diff({ foo: 'lol', bar: { baz: true }}, {}) // returns ["foo", "bar.baz"]

1
Працює як шарм, тільки що порядки obj1 та obj2 є важливими. Наприклад: diff({}, { foo: 'lol', bar: { baz: true }}) // returns []
amangpt777

2

Як було запропоновано, ось функція рекурсивного порівняння об'єктів. І ще трохи. Припускаючи, що основне використання такої функції - це перевірка об'єктів, я маю що сказати. Повне глибоке порівняння є поганою ідеєю, коли деякі відмінності не мають значення. Наприклад, сліпе глибоке порівняння у твердженнях TDD робить тести непотрібними крихкими. З цієї причини я хотів би ввести набагато більш цінну часткову різницю . Це рекурсивний аналог попереднього внеску в цю тему. Він ігнорує ключі , відсутні в а

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 дозволяє перевіряти очікувані значення при одночасному допуску до інших властивостей. Це саме те, що потрібно для автоматичного огляду. Це дозволяє будувати всілякі розширені твердження. Наприклад:

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

Повернення до повного рішення. Створення повної традиційної різниці з bdiff тривіально:

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

Виконання вище функції на двох складних об'єктах виведе щось подібне до цього:

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

Нарешті, для того, щоб мати уявлення про те, як значення відрізняються, ми можемо захотіти безпосередньо вирівняти () різницю . Для цього нам потрібна гірша версія bdiff, яка виводить синтаксично правильні шляхи:

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

Це виведе щось подібне до цього:

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

Ліцензія MIT;)


1

Завершуючи відповідь Адама Бодуха, ця враховує відмінності у властивостях

const differenceOfKeys = (...objects) =>
  _.difference(...objects.map(obj => Object.keys(obj)));
const differenceObj = (a, b) => 
  _.reduce(a, (result, value, key) => (
    _.isEqual(value, b[key]) ? result : [...result, key]
  ), differenceOfKeys(b, a));

1

Якщо вам потрібно лише порівняння ключів:

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

0

Ось простий Typescript з контролем глибинної різниці Лодаша, який створить новий об'єкт із лише різницями між старим об'єктом та новим об'єктом.

Наприклад, якщо ми мали:

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

Отриманим об'єктом буде:

const result: {b: 3};

Він також сумісний з багаторівневими глибокими об'єктами, для масивів може знадобитися певна настройка.

import * as _ from "lodash";

export const objectDeepDiff = (data: object | any, oldData: object | any) => {
  const record: any = {};
  Object.keys(data).forEach((key: string) => {
    // Checks that isn't an object and isn't equal
    if (!(typeof data[key] === "object" && _.isEqual(data[key], oldData[key]))) {
      record[key] = data[key];
    }
    // If is an object, and the object isn't equal
    if ((typeof data[key] === "object" && !_.isEqual(data[key], oldData[key]))) {
      record[key] = objectDeepDiff(data[key], oldData[key]);
    }
  });
  return record;
};

-1
var isEqual = function(f,s) {
  if (f === s) return true;

  if (Array.isArray(f)&&Array.isArray(s)) {
    return isEqual(f.sort(), s.sort());
  }
  if (_.isObject(f)) {
    return isEqual(f, s);
  }
  return _.isEqual(f, s);
};

Це недійсне. Ви не можете порівнювати об'єкти з ===безпосередньо, { a: 20 } === { a: 20 }повертається помилковим, оскільки він порівнює прототип. Більш правильний спосіб порівняння об'єктів - це перешивання їхJSON.stringify()
Герргот

якщо (f === s) повернути true; - тільки для рекурсії. Так a: 20} === {a: 20} поверне помилкове значення і перейде до наступної умови
хрестоносців

чому не тільки _.isEqual(f, s)? :)
Герргот

Це призведе до нескінченного циклу рекурсії, тому що якщо fце об'єкт, ви дістанетесь до if (_.isObject(f))вас, просто поверніться до функції і натисніть цю точку ще раз. Те саме стосуєтьсяf (Array.isArray(f)&&Array.isArray(s))
радію

-2

це ґрунтувалося на @JLavoie , використовуючи lodash

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

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


-2

просто використовуючи ванільний 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

введіть тут опис зображення


1
Цей метод не скаже вам, які атрибути відрізняються.
JLavoie

2
У цьому випадку атрибути порядок впливу на результат.
Віктор Олівейра

-2

На основі відповіді Шрідхара Гудімела тут оновлено таким чином, щоб потішити Потік:

"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;
};
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.