У меня есть два объекта: 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'}
Когда мы добираемся до свойств, которые являются массивами, это становится действительно сложным, поскольку массив [1,2,3]
должен считаться равным [2,3,1]
, что достаточно просто для массивов типов, основанных на значениях, таких как string, int и bool, но становится действительно сложным, когда дело доходит до массивов ссылочных типов, таких как объекты и массивы.
Примеры массивов, которые должны быть признаны равными:
[1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]
Довольно сложно не только проверить этот тип глубокого равенства значений, но и найти хороший способ представить изменения, которые могут быть.
Я взял ответ выше @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);
}
};
}();