Generic deep diff между двумя объектами

У меня есть два объекта: oldObj и newObj.

Данные в oldObj были использованы для заполнения формы, а newObj является результатом изменения пользователем данных в этой форме и ее отправки.

Оба объекта являются глубокими, т.е. имеют свойства, которые являются объектами или массивами объектов и т.д. - они могут иметь n уровней глубины, поэтому алгоритм diff должен быть рекурсивным.

Теперь мне нужно не просто выяснить, что было изменено (в смысле добавлено/обновлено/удалено) от oldObj к newObj, но и как это лучше представить.

До сих пор я думал просто создать genericDeepDiffBetweenObjects метод, который бы возвращал объект вида {add:{...},upd:{...},del:{...}}, но потом я подумал: кому-то это уже должно быть нужно.

Итак... кто-нибудь знает библиотеку или кусок кода, который может сделать это и, возможно, имеет еще лучший способ представления разницы (таким образом, чтобы JSON оставался сериализуемым)?

Обновление:

Я придумал лучший способ представления обновленных данных, используя ту же объектную структуру, что и newObj, но превратив все значения свойств в объекты на форме:

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

Итак, если newObj. prop1 = 'новое значение' и oldObj.prop1 = 'старое значение', то returnObj.prop1 = {type: 'update', data: 'new value'}

Update 2:

Когда мы добираемся до свойств, которые являются массивами, это становится действительно сложным, поскольку массив [1,2,3] должен считаться равным [2,3,1], что достаточно просто для массивов типов, основанных на значениях, таких как string, int и bool, но становится действительно сложным, когда дело доходит до массивов ссылочных типов, таких как объекты и массивы.

Примеры массивов, которые должны быть признаны равными:

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

Довольно сложно не только проверить этот тип глубокого равенства значений, но и найти хороший способ представить изменения, которые могут быть.

193
задан wjandrea 21 October 2018 в 10:47
поделиться

1 ответ

Я взял ответ выше @sbgoran и изменил его для моего случая то же как необходимый вопрос, для обработки массивов как наборов (т.е. порядок не важен для разности)

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
ответ дан 23 November 2019 в 05:28
поделиться
Другие вопросы по тегам:

Похожие вопросы: