Как сделать глубокое сравнение между 2 объектами с помощью lodash?


309

У меня есть 2 вложенных объекта, которые отличаются друг от друга, и мне нужно знать, имеют ли они различия в одном из своих вложенных свойств.

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 ({а: 1, Ь: 2}) == JSON.stringify ({Ь: 2, а: 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может быть довольно сложно. Если вы скопируете объект и измените там некоторые значения, он все равно будет показывать true, потому что ссылка такая же. Поэтому следует быть осторожным, используя эту функцию.
'51

23
@oruckdeschel, если ссылка одна и та же, это тот же объект. следовательно, он равен. этот указатель хитрый, а не лодаш. Лодаш потрясающий.
парень mograbi

265

Если вам нужно узнать, какие свойства различаются, используйте redu () :

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

36
Обратите внимание, что это будет выводить только первый уровень различных свойств. (Так что это не очень глубоко в смысле вывода свойств, которые отличаются.)
Bloke

16
Кроме того, это не подберет свойства в b, которые не в a.
Эд Стауб

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

1
Версия, заключающая в себе ключ: значениеlet edited = _.reduce(a, function(result, value, key) { return _.isEqual(value, b[key]) ? result : result.concat( { [key]: value } ); }, []);
Алина Матос,

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

Если вы не заботитесь о вложенных объектах и ​​хотите пропустить lodash, вы можете заменить _.isEqualобычное сравнение значений, например obj1[key] === obj2[key].


Этот выбранный ответ является правильным только для проверки равенства. Если вам нужно знать, в чем различия, нет очевидного способа перечислить их, но этот ответ довольно хорош, просто дать список ключей свойств верхнего уровня, где есть разница. (И это дает ответ как функцию, которая делает его пригодным для использования.)
Зигфрид

В чем разница между этим и использованием _.isEqual (obj1, obj2)? Что делает добавление проверки hasOwnProperty, чего не делает _.isEqual? Я предполагал, что если бы у obj1 было свойство, которого у obj2 не было, _.isEqual не вернул бы true ..?
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функция возвращала неверный diff для объектов, содержащих undefined, как в compare({disabled: undefined}, {disabled: undefined}). На самом деле, старая версия также имела проблемы с null; Вы можете избежать подобных проблем, всегда используя ===и!== вместо ==и !=.
Рори О'Кейн

23

Вот краткое решение:

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

7
Кажется, не работает с объектами для меня. Вместо этого возвращает пустой массив.
Tomhughes

2
Также получаю пустой массив с Lodash 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способ использования , он будет работать для всех сравнений ...

  • Примечание. Этот метод поддерживает сравнение массивов, буферов массивов, логических значений, объектов * date, объектов ошибок, карт, чисел, 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 / underscore, я написал этот код и отлично работает для меня для глубокого сравнения 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 делает тесты ненужными хрупкими. По этой причине я хотел бы представить гораздо более ценный частичный diff . Это рекурсивный аналог предыдущего вклада в эту тему. Он игнорирует ключи , отсутствующие в а

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

Возвращаясь к полному решению. Создать полный традиционный diff с помощью 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"
 ]

Наконец, для того, чтобы получить представление о различии значений, мы можем напрямую вычислить () вывод diff. Для этого нам нужна более уродливая версия 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 с Lodash для проверки глубинных различий, который создаст новый объект с теми же различиями, что и между старым и новым объектами.

Например, если бы мы имели:

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 }вернет false, потому что он сравнивает прототип. Более правильный способ в первую очередь сравнить объекты, чтобы обернуть их вJSON.stringify()
Herrgott

if (f === s) возвращает true; - только для рекурсии. Да a: 20} === {a: 20} вернет false и перейдет к следующему условию
Crusader

почему не только _.isEqual(f, s)? :)
Herrgott

Это приведет к бесконечному циклу рекурсии, потому что если 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

просто с помощью Vanilla 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

Чтобы развить ответ Шридхара Гудимелы , здесь он обновляется таким образом, чтобы сделать 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;
};
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.