Javascript 两个对象之间的通用深度差异
我有两个对象:Javascript 两个对象之间的通用深度差异,javascript,object,compare,Javascript,Object,Compare,我有两个对象:oldObj和newObj oldObj中的数据用于填充表单,newObj是用户更改此表单中的数据并提交的结果 这两个对象都是深的,即它们的属性是对象或对象数组等-它们可以是n层深的,因此diff算法需要递归 现在,我不仅需要弄清楚从oldObj到newObj的更改(如添加/更新/删除)是什么,还需要弄清楚如何最好地表示它 到目前为止,我的想法是只构建一个genericDeepDiffBetweenObjects方法,该方法将以{add:{…},upd:{…},del:{…}的形式
oldObj
和newObj
oldObj
中的数据用于填充表单,newObj
是用户更改此表单中的数据并提交的结果
这两个对象都是深的,即它们的属性是对象或对象数组等-它们可以是n层深的,因此diff算法需要递归
现在,我不仅需要弄清楚从oldObj
到newObj
的更改(如添加/更新/删除)是什么,还需要弄清楚如何最好地表示它
到目前为止,我的想法是只构建一个genericDeepDiffBetweenObjects
方法,该方法将以{add:{…},upd:{…},del:{…}
的形式返回一个对象,但我想:以前肯定有人需要这个方法
所以。。。是否有人知道一个库或一段代码可以做到这一点,并且可能有更好的方法来表示差异(以一种仍然可以JSON序列化的方式)
更新:
我想到了一种更好的方法来表示更新后的数据,方法是使用与newObj
相同的对象结构,但将所有属性值转换为表单上的对象:
{type: '<update|create|delete>', data: <propertyValue>}
不仅检查这种类型的深度值相等非常复杂,而且找出一种表示可能发生的更改的好方法也非常复杂。我使用这段代码来完成您描述的任务:
function mergeRecursive(obj1, obj2) {
for (var p in obj2) {
try {
if(obj2[p].constructor == Object) {
obj1[p] = mergeRecursive(obj1[p], obj2[p]);
}
// Property in destination object set; update its value.
else if (Ext.isArray(obj2[p])) {
// obj1[p] = [];
if (obj2[p].length < 1) {
obj1[p] = obj2[p];
}
else {
obj1[p] = mergeRecursive(obj1[p], obj2[p]);
}
}else{
obj1[p] = obj2[p];
}
} catch (e) {
// Property in destination object not set; create it and set its value.
obj1[p] = obj2[p];
}
}
return obj1;
}
函数合并递归(obj1、obj2){
用于(obj2中的var p){
试一试{
if(obj2[p]。构造函数==对象){
obj1[p]=合并递归(obj1[p],obj2[p]);
}
//属性;更新其值。
else if(Ext.isArray(obj2[p])){
//obj1[p]=[];
if(obj2[p]。长度<1){
obj1[p]=obj2[p];
}
否则{
obj1[p]=合并递归(obj1[p],obj2[p]);
}
}否则{
obj1[p]=obj2[p];
}
}捕获(e){
//未设置目标对象中的属性;请创建它并设置其值。
obj1[p]=obj2[p];
}
}
返回obj1;
}
这将获得一个新对象,该对象将合并表单中旧对象和新对象之间的所有更改
function mergeRecursive(obj1, obj2) {
for (var p in obj2) {
try {
if(obj2[p].constructor == Object) {
obj1[p] = mergeRecursive(obj1[p], obj2[p]);
}
// Property in destination object set; update its value.
else if (Ext.isArray(obj2[p])) {
// obj1[p] = [];
if (obj2[p].length < 1) {
obj1[p] = obj2[p];
}
else {
obj1[p] = mergeRecursive(obj1[p], obj2[p]);
}
}else{
obj1[p] = obj2[p];
}
} catch (e) {
// Property in destination object not set; create it and set its value.
obj1[p] = obj2[p];
}
}
return obj1;
}
函数合并递归(obj1、obj2){
用于(obj2中的var p){
试一试{
if(obj2[p]。构造函数==对象){
obj1[p]=合并递归(obj1[p],obj2[p]);
}
//属性;更新其值。
else if(Ext.isArray(obj2[p])){
//obj1[p]=[];
if(obj2[p]。长度<1){
obj1[p]=obj2[p];
}
否则{
obj1[p]=合并递归(obj1[p],obj2[p]);
}
}否则{
obj1[p]=obj2[p];
}
}捕获(e){
//未设置目标对象中的属性;请创建它并设置其值。
obj1[p]=obj2[p];
}
}
返回obj1;
}
这将获得一个新对象,该对象将合并表单中旧对象和新对象之间的所有更改我编写了一个小类,它正在执行您想要的操作,您可以测试它 唯一不同于你的建议的是我不考虑
[1,[{c: 1},2,3],{a:'hey'}]
及
是一样的,因为我认为数组是不相等的,若它们的元素顺序不一样。当然,如果需要,这可以改变。此外,还可以进一步增强此代码,使其将函数作为参数,用于根据传递的原语值以任意方式格式化diff对象(现在此工作由“compareValues”方法完成)
var deepDiffMapper=函数(){
返回{
已创建的值:“已创建”,
值_已更新:“已更新”,
值\u已删除:“已删除”,
值_未更改:“未更改”,
映射:功能(obj1、obj2){
if(this.isFunction(obj1)| this.isFunction(obj2)){
抛出“无效参数。给定函数,应为对象”;
}
if(this.isValue(obj1)| | this.isValue(obj2)){
返回{
类型:this.compareValue(obj1、obj2),
数据:obj1==未定义?obj2:obj1
};
}
var diff={};
用于(obj1中的var键){
if(此.isFunction(obj1[key])){
继续;
}
var值2=未定义;
如果(obj2[键]!==未定义){
value2=obj2[键];
}
diff[key]=此映射(obj1[key],值2);
}
用于(obj2中的var键){
if(此.isFunction(obj2[key])| | diff[key]!==未定义){
继续;
}
diff[key]=此.map(未定义,obj2[key]);
}
返回差;
},
比较值:函数(值1、值2){
如果(值1==值2){
返回此.VALUE\u不变;
}
if(this.isDate(value1)&&this.isDate(value2)&&value1.getTime()==value2.getTime()){
返回此.VALUE\u不变;
}
如果(值1==未定义){
返回创建的此.VALUE\u;
}
如果(值2==未定义){
返回此值。\u已删除;
}
返回此值。\u已更新;
},
isFunction:function(x){
返回Object.prototype.toString.call(x)='[Object Function]';
},
isArray:函数(x){
返回Object.prototype.toString.call(x)='[objectarray]';
},
isDate:函数(x){
返回Object.prototype.toString.call(x)='[Object Date]';
},
isObject:函数(x){
返回Object.prototype.toString.call(x)=='[Object]';
},
isValue:函数(x){
return!this.isObject(x)&!this.isArray(x);
}
}
}();
var result=deepDiffMapper.map({
a:我是unc
[{a:'hey'},1,[3,{c: 1},2]]
var o1 = {a: 1, b: 2, c: 2},
o2 = {a: 2, b: 1, c: 2};
_.omit(o1, function(v,k) { return o2[k] === v; })
{a: 1, b: 2}
function diff(a,b) {
var r = {};
_.each(a, function(v,k) {
if(b[k] === v) return;
// but what if it returns an empty object? still attach?
r[k] = _.isObject(v)
? _.diff(v, b[k])
: v
;
});
return r;
}
(function(_) {
function deepDiff(a, b, r) {
_.each(a, function(v, k) {
// already checked this or equal...
if (r.hasOwnProperty(k) || b[k] === v) return;
// but what if it returns an empty object? still attach?
r[k] = _.isObject(v) ? _.diff(v, b[k]) : v;
});
}
/* the function */
_.mixin({
diff: function(a, b) {
var r = {};
deepDiff(a, b, r);
deepDiff(b, a, r);
return r;
}
});
})(_.noConflict());
var a = [{a:1,b:2,c:3}, {x:1,y: 2, z:3}, {w:9,q:8,r:7}]
var b = [{a:1,b:2,c:3},{t:4,y:5,u:6},{x:1,y:'3',z:3},{t:9,y:9,u:9},{w:9,q:8,r:7}]
var diffs = odiff(a,b)
/* diffs now contains:
[{type: 'add', path:[], index: 2, vals: [{t:9,y:9,u:9}]},
{type: 'set', path:[1,'y'], val: '3'},
{type: 'add', path:[], index: 1, vals: [{t:4,y:5,u:6}]}
]
*/
<script type="text" src="https://unpkg.com/recursive-diff@latest/dist/recursive-diff.min.js"/>
<script type="text/javascript">
const ob1 = {a:1, b: [2,3]};
const ob2 = {a:2, b: [3,3,1]};
const delta = recursiveDiff.getDiff(ob1,ob2);
/* console.log(delta) will dump following data
[
{path: ['a'], op: 'update', val: 2}
{path: ['b', '0'], op: 'update',val: 3},
{path: ['b',2], op: 'add', val: 1 },
]
*/
const ob3 = recursiveDiff.applyDiff(ob1, delta); //expect ob3 is deep equal to ob2
</script>
const diff = require('recursive-diff');
const ob1 = {a: 1}, ob2: {b:2};
const diff = diff.getDiff(ob1, ob2);
// To only return the difference
var difference = diff(newValue, oldValue);
// To exclude certain properties
var difference = diff(newValue, oldValue, [newValue.prop1, newValue.prop2, newValue.prop3]);
// ObjectKey: ["DataType, DefaultValue"]
reference = {
a : ["string", 'Defaul value for "a"'],
b : ["number", 300],
c : ["boolean", true],
d : {
da : ["boolean", true],
db : ["string", 'Defaul value for "db"'],
dc : {
dca : ["number", 200],
dcb : ["string", 'Default value for "dcb"'],
dcc : ["number", 500],
dcd : ["boolean", true]
},
dce : ["string", 'Default value for "dce"'],
},
e : ["number", 200],
f : ["boolean", 0],
g : ["", 'This is an internal extra parameter']
};
userOptions = {
a : 999, //Only string allowed
//b : ["number", 400], //User missed this parameter
c: "Hi", //Only lower case or case insitive in quotes true/false allowed.
d : {
da : false,
db : "HelloWorld",
dc : {
dca : 10,
dcb : "My String", //Space is not allowed for ID attr
dcc: "3thString", //Should not start with numbers
dcd : false
},
dce: "ANOTHER STRING",
},
e: 40,
f: true,
};
function compare(ref, obj) {
var validation = {
number: function (defaultValue, userValue) {
if(/^[0-9]+$/.test(userValue))
return userValue;
else return defaultValue;
},
string: function (defaultValue, userValue) {
if(/^[a-z][a-z0-9-_.:]{1,51}[^-_.:]$/i.test(userValue)) //This Regex is validating HTML tag "ID" attributes
return userValue;
else return defaultValue;
},
boolean: function (defaultValue, userValue) {
if (typeof userValue === 'boolean')
return userValue;
else return defaultValue;
}
};
for (var key in ref)
if (obj[key] && obj[key].constructor && obj[key].constructor === Object)
ref[key] = compare(ref[key], obj[key]);
else if(obj.hasOwnProperty(key))
ref[key] = validation[ref[key][0]](ref[key][1], obj[key]); //or without validation on user enties => ref[key] = obj[key]
else ref[key] = ref[key][1];
return ref;
}
//console.log(
alert(JSON.stringify( compare(reference, userOptions),null,2 ))
//);
{
"a": "Defaul value for \"a\"",
"b": 300,
"c": true,
"d": {
"da": false,
"db": "Defaul value for \"db\"",
"dc": {
"dca": 10,
"dcb": "Default value for \"dcb\"",
"dcc": 500,
"dcd": false
},
"dce": "Default value for \"dce\""
},
"e": 40,
"f": true,
"g": "This is an internal extra parameter"
}
*/
let o1 = {
one: 1,
two: 2,
three: 3
}
let o2 = {
two: 2,
three: 3,
four: 4
}
let diff = Object.keys(o2).reduce((diff, key) => {
if (o1[key] === o2[key]) return diff
return {
...diff,
[key]: o2[key]
}
}, {})
const oldState = {id:'170',name:'Ivab',secondName:'Ivanov',weight:45};
const newState = {id:'170',name:'Ivanko',secondName:'Ivanov',age:29};
const keysObj1 = R.keys(newState)
const filterFunc = key => {
const value = R.eqProps(key,oldState,newState)
return {[key]:value}
}
const result = R.map(filterFunc, keysObj1)
[{"id":true}, {"name":false}, {"secondName":true}, {"age":false}]
const diff = require("deep-object-diff").diff;
let differences = diff(obj2, obj1);
const diff = require("deep-object-diff").diff;
const lhs = {
foo: {
bar: {
a: ['a', 'b'],
b: 2,
c: ['x', 'y'],
e: 100 // deleted
}
},
buzz: 'world'
};
const rhs = {
foo: {
bar: {
a: ['a'], // index 1 ('b') deleted
b: 2, // unchanged
c: ['x', 'y', 'z'], // 'z' added
d: 'Hello, world!' // added
}
},
buzz: 'fizz' // updated
};
console.log(diff(lhs, rhs)); // =>
/*
{
foo: {
bar: {
a: {
'1': undefined
},
c: {
'2': 'z'
},
d: 'Hello, world!',
e: undefined
}
},
buzz: 'fizz'
}
*/
// Get updated values (including new values)
var updatedValuesIncl = _.omitBy(curr, (value, key) => _.isEqual(last[key], value));
// Get updated values (excluding new values)
var updatedValuesExcl = _.omitBy(curr, (value, key) => (!_.has(last, key) || _.isEqual(last[key], value)));
// Get old values (by using updated values)
var oldValues = Object.keys(updatedValuesIncl).reduce((acc, key) => { acc[key] = last[key]; return acc; }, {});
// Get newly added values
var newCreatedValues = _.omitBy(curr, (value, key) => _.has(last, key));
// Get removed values
var deletedValues = _.omitBy(last, (value, key) => _.has(curr, key));
// Then you can group them however you want with the result
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);
}
};
}();
isNullBlankOrUndefined = function (o) {
return (typeof o === "undefined" || o == null || o === "");
}
/**
* Deep diff between two object, using lodash
* @param {Object} object Object compared
* @param {Object} base Object to compare with
* @param {Object} ignoreBlanks will not include properties whose value is null, undefined, etc.
* @return {Object} Return a new object who represent the diff
*/
objectDifference = function (object, base, ignoreBlanks = false) {
if (!lodash.isObject(object) || lodash.isDate(object)) return object // special case dates
return lodash.transform(object, (result, value, key) => {
if (!lodash.isEqual(value, base[key])) {
if (ignoreBlanks && du.isNullBlankOrUndefined(value) && isNullBlankOrUndefined( base[key])) return;
result[key] = lodash.isObject(value) && lodash.isObject(base[key]) ? objectDifference(value, base[key]) : value;
}
});
}
export interface ObjectComparison {
added: {};
updated: {
[propName: string]: Change;
};
removed: {};
unchanged: {};
}
export interface Change {
oldValue: any;
newValue: any;
}
export class ObjectUtils {
static diff(o1: {}, o2: {}, deep = false): ObjectComparison {
const added = {};
const updated = {};
const removed = {};
const unchanged = {};
for (const prop in o1) {
if (o1.hasOwnProperty(prop)) {
const o2PropValue = o2[prop];
const o1PropValue = o1[prop];
if (o2.hasOwnProperty(prop)) {
if (o2PropValue === o1PropValue) {
unchanged[prop] = o1PropValue;
} else {
updated[prop] = deep && this.isObject(o1PropValue) && this.isObject(o2PropValue) ? this.diff(o1PropValue, o2PropValue, deep) : {newValue: o2PropValue};
}
} else {
removed[prop] = o1PropValue;
}
}
}
for (const prop in o2) {
if (o2.hasOwnProperty(prop)) {
const o1PropValue = o1[prop];
const o2PropValue = o2[prop];
if (o1.hasOwnProperty(prop)) {
if (o1PropValue !== o2PropValue) {
if (!deep || !this.isObject(o1PropValue)) {
updated[prop].oldValue = o1PropValue;
}
}
} else {
added[prop] = o2PropValue;
}
}
}
return { added, updated, removed, unchanged };
}
/**
* @return if obj is an Object, including an Array.
*/
static isObject(obj: any) {
return obj !== null && typeof obj === 'object';
}
}
ObjectUtils.diff(
{
a: 'a',
b: 'b',
c: 'c',
arr: ['A', 'B'],
obj: {p1: 'p1', p2: 'p2'}
},
{
b: 'x',
c: 'c',
arr: ['B', 'C'],
obj: {p2: 'p2', p3: 'p3'},
d: 'd'
},
);
{
added: {d: 'd'},
updated: {
b: {oldValue: 'b', newValue: 'x'},
arr: {oldValue: ['A', 'B'], newValue: ['B', 'C']},
obj: {oldValue: {p1: 'p1', p2: 'p2'}, newValue: {p2: 'p2', p3: 'p3'}}
},
removed: {a: 'a'},
unchanged: {c: 'c'},
}
{
added: {d: 'd'},
updated: {
b: {oldValue: 'b', newValue: 'x'},
arr: {
added: {},
removed: {},
unchanged: {},
updated: {
0: {oldValue: 'A', newValue: 'B'},
1: {oldValue: 'B', newValue: 'C', }
}
},
obj: {
added: {p3: 'p3'},
removed: {p1: 'p1'},
unchanged: {p2: 'p2'},
updated: {}
}
},
removed: {a: 'a'},
unchanged: {c: 'c'},
}
var deepDiffMapper = function () {
return {
VALUE_CREATED: 'created',
VALUE_UPDATED: 'updated',
VALUE_DELETED: 'deleted',
VALUE_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)) {
let returnObj = {
type: this.compareValues(obj1, obj2),
original: obj1,
updated: obj2,
};
if (returnObj.type != this.VALUE_UNCHANGED) {
return returnObj;
}
return undefined;
}
var diff = {};
let foundKeys = {};
for (var key in obj1) {
if (this.isFunction(obj1[key])) {
continue;
}
var value2 = undefined;
if (obj2[key] !== undefined) {
value2 = obj2[key];
}
let mapValue = this.map(obj1[key], value2);
foundKeys[key] = true;
if (mapValue) {
diff[key] = mapValue;
}
}
for (var key in obj2) {
if (this.isFunction(obj2[key]) || foundKeys[key] !== undefined) {
continue;
}
let mapValue = this.map(undefined, obj2[key]);
if (mapValue) {
diff[key] = mapValue;
}
}
//2020-06-13: object length code copied from https://stackoverflow.com/a/13190981/2336212
if (Object.keys(diff).length > 0) {
return diff;
}
return undefined;
},
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);
}
}
}();
function deepCompare(obj1, obj2) {
var diffObj = Array.isArray(obj2) ? [] : {}
Object.getOwnPropertyNames(obj2).forEach(function(prop) {
if (typeof obj2[prop] === 'object') {
diffObj[prop] = deepCompare(obj1[prop], obj2[prop])
// if it's an array with only length property => empty array => delete
// or if it's an object with no own properties => delete
if (Array.isArray(diffObj[prop]) && Object.getOwnPropertyNames(diffObj[prop]).length === 1 || Object.getOwnPropertyNames(diffObj[prop]).length === 0) {
delete diffObj[prop]
}
} else if(obj1[prop] !== obj2[prop]) {
diffObj[prop] = obj2[prop]
}
});
return diffObj
}
export const getObjectDifference = <T extends {}>(originalObject: T, newObject: T) => {
const sameProperties: string[] = [];
Object.entries(originalObject).forEach(original => {
Object.entries(newObject).forEach(newObj => {
if (original[0] === newObj[0]) {
if (original[1] === newObj[1])
sameProperties.push(newObj[0]);
}
});
});
const objectDifference: T = omit(newObject, sameProperties) as T;
if (isEmpty(objectDifference))
return null;
else
return objectDifference; }