我有两个对象:oldObj和newObj。
oldObj中的数据用于填充表单,而newObj是用户更改该表单中的数据并提交的结果。
这两个物体都很深。它们具有对象或对象数组等属性-它们可以有n层深,因此diff算法需要递归。
现在我不仅需要弄清楚从oldObj到newObj更改了什么(如添加/更新/删除),而且还需要知道如何最好地表示它。
到目前为止,我的想法只是构建一个通用的deepdiffbetweenobjects方法,该方法将返回窗体上的对象{add:{…},upd:{…},del:{…但我转念一想:以前一定有人需要它。
所以…有没有人知道一个库或一段代码可以做到这一点,并且可能有更好的方式来表示差异(以一种仍然是JSON可序列化的方式)?
更新:
我想到了一个更好的方法来表示更新的数据,通过使用与newObj相同的对象结构,但将所有属性值转换为窗体上的对象:
{type: '<update|create|delete>', data: <propertyValue>}
如果newObj。prop1 = 'new value'和oldObj。prop1 = 'old value'它将设置returnbj。Prop1 = {type: 'update', data: 'new value'}
更新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]]
不仅要检查这种类型的深度值相等相当复杂,而且要找出一种表示可能发生的变化的好方法。
我采用@sbgoran上面的答案,并根据我的情况修改了它,以将数组视为集合(即顺序对于diff不重要)
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);
}
};
}();
我写了一个小类来做你想做的,你可以在这里测试。
唯一不同于你的建议的是我不考虑
[1,[{c: 1},2,3],{a:'hey'}]
and
[{a:'hey'},1,[3,{c: 1},2]]
相等,因为我认为数组中元素的顺序不相等。当然,如果需要,这可以改变。此外,这段代码还可以进一步增强,将函数作为参数,用于基于传递的原语值以任意方式格式化diff对象(现在这项工作由"compareValues"方法完成)。
var deepDiffMapper = function () {
return {
VALUE_CREATED: 'created',
VALUE_UPDATED: 'updated',
VALUE_DELETED: 'deleted',
VALUE_UNCHANGED: 'unchanged',
map: function(obj1, obj2) {
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: obj1 === undefined ? obj2 : obj1
};
}
var diff = {};
for (var key in obj1) {
if (this.isFunction(obj1[key])) {
continue;
}
var value2 = undefined;
if (obj2[key] !== undefined) {
value2 = obj2[key];
}
diff[key] = this.map(obj1[key], value2);
}
for (var key in obj2) {
if (this.isFunction(obj2[key]) || diff[key] !== undefined) {
continue;
}
diff[key] = this.map(undefined, obj2[key]);
}
return diff;
},
compareValues: function (value1, value2) {
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) {
return Object.prototype.toString.call(x) === '[object Function]';
},
isArray: function (x) {
return Object.prototype.toString.call(x) === '[object Array]';
},
isDate: function (x) {
return Object.prototype.toString.call(x) === '[object Date]';
},
isObject: function (x) {
return Object.prototype.toString.call(x) === '[object Object]';
},
isValue: function (x) {
return !this.isObject(x) && !this.isArray(x);
}
}
}();
var result = deepDiffMapper.map({
a: 'i am unchanged',
b: 'i am deleted',
e: {
a: 1,
b: false,
c: null
},
f: [1, {
a: 'same',
b: [{
a: 'same'
}, {
d: 'delete'
}]
}],
g: new Date('2017.11.25')
}, {
a: 'i am unchanged',
c: 'i am created',
e: {
a: '1',
b: '',
d: 'created'
},
f: [{
a: 'same',
b: [{
a: 'same'
}, {
c: 'create'
}]
}, 1],
g: new Date('2017.11.25')
});
console.log(result);
下面的方法将创建一个只更改字段的新对象
const findDiff = (obj1, obj2) => {
const isNativeType1 = typeof obj1 !== "object";
const isNativeType2 = typeof obj2 !== "object";
if (isNativeType1 && isNativeType2) {
return obj1 === obj2 ? null : obj2;
}
if (isNativeType1 && !isNativeType2) {
return obj2;
}
if (!isNativeType1 && isNativeType2) {
return obj2;
}
const isArray1 = Array.isArray(obj1);
const isArray2 = Array.isArray(obj2);
if (isArray1 && isArray2) {
const firstLenght = obj1.length;
const secondLenght = obj2.length;
const hasSameLength = firstLenght === secondLenght;
if (!hasSameLength) return obj2;
let hasChange = false;
for (let index = 0; index < obj1.length; index += 1) {
const element1 = obj1[index];
const element2 = obj2[index];
const changed = findDiff(element1, element2);
if (changed) {
hasChange = true;
}
}
return hasChange ? obj2 : null;
}
if (isArray1 || isArray2) return obj2;
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
const hasSameKeys = keys1.length === keys2.length;
if (!hasSameKeys) {
const retObj = { ...obj2 };
for (let index = 0; index < keys1.length; index += 1) {
const key = keys1[index];
if (!keys2.includes(key)) {
retObj[key] = null;
// eslint-disable-next-line no-continue
continue;
}
delete retObj[key];
}
return retObj;
}
let hasChange = false;
const retObj = {};
for (let index = 0; index < keys1.length; index += 1) {
const key = keys1[index];
const element1 = obj1[key];
const element2 = obj2[key];
const changed = findDiff(element1, element2);
if (changed) {
hasChange = true;
}
if (changed) {
retObj[key] = changed;
}
}
return hasChange ? retObj : null;
};
console.log(
JSON.stringify(findDiff(
{
a: 1,
b: 2,
c: {
a: ['1', 'b', { a: 'b', c: false }, true],
},
},
{
a: 1,
b: 2,
c: {
a: ['1','b', { a: 'b', c: true }, true],
},
}
), null, 2)
);
2022年更新:
我想出了一个非常简单的算法来解决大多数边缘情况:
使物体变平
简单地比较两个扁平对象并创建一个扁平的diff对象
平展diff对象
如果你保存了被压平的物体,你就可以重复使用它,并在你真正需要的时候进行“反压平…”
let oldObject = {var1:'value1', var2:{ var1:'value1', var2:'value2'},var3:'value3'};
let newObject = {var2:{ var1:'value11', var3:'value3'},var3:'value3'};
let flatOldObject = flattenObject(oldObject)
/*
{
'var1':'value1',
'var2.var1':'value1',
'var2.var2':'value2',
'var3':'value3'
}
*/
let flatNewObject = flattenObject(newObject)
/*
{
'var2.var1':'value11',
'var2.var3':'value3',
'var3':'value3'
}
*/
let flatDiff = diffFlatten(flatOldObject, flatNewObject)
let [updated,removed] = flatDiff
/*
updated = {
'var2.var1':'value11',
'var2.var3':'value3'
}
removed = {
'var1':'value1'
}
*/
当然,您可以为这些步骤提供您的实现。但我的想法是:
实现
function flattenObject(obj) {
const object = Object.create(null);
const path = [];
const isObject = (value) => Object(value) === value;
function dig(obj) {
for (let [key, value] of Object.entries(obj)) {
path.push(key);
if (isObject(value)) dig(value);
else object[path.join('.')] = value;
path.pop();
}
}
dig(obj);
return object;
}
function diffFlatten(oldFlat, newFlat) {
const updated = Object.assign({}, oldFlat);
const removed = Object.assign({}, newFlat);
/**delete the unUpdated keys*/
for (let key in newFlat) {
if (newFlat[key] === oldFlat[key]) {
delete updated[key];
delete removed[key];
}
}
return [updated, removed];
}
function unflatenObject(flattenObject) {
const unFlatten = Object.create(null);
for (let [stringKeys, value] of Object.entries(flattenObject)) {
let chain = stringKeys.split('.')
let object = unFlatten
for (let [i, key] of chain.slice(0, -1).entries()) {
if (!object[key]) {
let needArray = Number.isInteger(Number(chain[+i + 1]))
object[key] = needArray ? [] : Object.create(null)
}
object = object[key];
}
let lastkey = chain.pop();
object[lastkey] = value;
}
return unFlatten;
}