How to do a deep comparison between 2 objects with lodash?
我有两个不同的嵌套对象,我需要知道它们在其中一个嵌套属性中是否存在差异。
1 2 3 4 5 6 7 8 | var a = {}; var b = {}; a.prop1 = 2; a.prop2 = { prop3: 2 }; b.prop1 = 2; b.prop2 = { prop3: 3 }; |
对象可能更复杂,具有更多的嵌套属性。但这是一个很好的例子。我可以选择使用递归函数或与lodash一起使用…
一个简单而优雅的解决方案是使用
1 2 3 4 5 6 7 8 9 10 | 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/
如果需要知道哪些属性不同,请使用reduce():
1 2 3 4 5 | _.reduce(a, function(result, value, key) { return _.isEqual(value, b[key]) ? result : result.concat(key); }, []); // → ["prop2" ] |
对于任何在这条线上绊倒的人,这里有一个更完整的解决方案。它将比较两个对象,并为您提供所有属性的键,这些属性要么仅在对象1中,要么仅在对象2中,要么都在对象1和对象2中,但具有不同的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /* * 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; } |
下面是一个输出示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 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,可以用
基于AdamBoduch的回答,我编写了这个函数,它从最深层的意义上比较两个对象,返回具有不同值的路径以及一个或另一个对象缺少的路径。
代码的编写并没有考虑到效率问题,在这方面的改进是最受欢迎的,但下面是基本形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | 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; } |
您可以使用此代码段尝试代码(建议以整页模式运行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | 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; } var a_editor = new JSONEditor($('#a')[0], { name: 'a', mode: 'code' }); var b_editor = new JSONEditor($('#b')[0], { name: 'b', mode: 'code' }); var a = { same: 1, different: 2, missing_from_b: 3, missing_nested_from_b: { x: 1, y: 2 }, nested: { same: 1, different: 2, missing_from_b: 3 } } var b = { same: 1, different: 99, missing_from_a: 3, missing_nested_from_a: { x: 1, y: 2 }, nested: { same: 1, different: 99, missing_from_a: 3 } } a_editor.set(a); b_editor.set(b); var result_editor = new JSONEditor($('#result')[0], { name: 'result', mode: 'view' }); var do_compare = function() { var a = a_editor.get(); var b = b_editor.get(); result_editor.set(compare(a, b)); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #objects {} #objects section { margin-bottom: 10px; } #objects section h1 { background: #444; color: white; font-family: monospace; display: inline-block; margin: 0; padding: 5px; } .jsoneditor-outer, .ace_editor { min-height: 230px !important; } button:hover { background: orangered; } button { cursor: pointer; background: red; color: white; text-align: left; font-weight: bold; border: 5px solid crimson; outline: 0; padding: 10px; margin: 10px 0px; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <link href="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/5.5.10/jsoneditor.min.css" rel="stylesheet" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/5.5.10/jsoneditor.min.js"> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"> <section> a (first object) </section> <section> b (second object) </section> <button onClick="do_compare()">compare</button> <section> result </section> |
下面是一个简明的解决方案:
1 | _.differenceWith(a, b, _.isEqual); |
要递归地显示一个对象与另一个对象的不同之处,您可以使用uureduce与u.isequal和.isplainobject组合使用。在这种情况下,您可以比较A与B的区别或B与A的区别:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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); |
1 |
此代码返回一个对象,该对象具有所有具有不同值的属性以及两个对象的值。有助于记录差异。
1 2 3 4 5 6 7 | 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; }, {}); |
使用(嵌套)属性模板进行深度比较以检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | 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) |
这将在控制台中工作。如果需要,可以添加数组支持
如果不使用lodash/underline,我已经编写了这段代码,并且可以很好地将object1与object2进行深入比较。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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; } |
我刺穿了Adam Boduch的代码以输出深度差异-这完全是未经测试的,但片段如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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 2 3 | _.reduce(a, function(result, value, key) { return b[key] === undefined ? key : [] }, []); |
完成亚当·博杜克的回答后,这个问题考虑到了属性的差异。
1 2 3 4 5 6 | 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)); |
正如所要求的,这里有一个递归的对象比较函数。还有一点。假设这种功能的主要用途是对象检查,我有话要说。当某些差异无关紧要时,完全深入比较是一个坏主意。例如,TDD断言中的盲深度比较会使测试变得不必要的脆弱。出于这个原因,我想介绍一个更有价值的部分diff,它是对这个线程先前贡献的递归模拟。它忽略不存在于
1 2 3 4 5 6 | 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允许在容忍其他属性的同时检查期望值,这正是您想要的自动检查。这允许构建各种高级断言。例如:
1 2 3 4 5 | 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); |
返回完整的解决方案。使用bdiff构建完整的传统diff非常简单:
1 2 3 4 5 6 | 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)); }; |
在两个复杂对象上运行上述函数将输出类似于以下内容的内容:
1 2 3 4 5 6 7 8 9 | [ " < 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输出进行eval()。为此,我们需要一个更糟糕的bdiff版本,它输出语法正确的路径:
1 2 3 4 5 6 7 8 9 10 11 | // 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))))); |
它将输出类似于以下内容的内容:
1 2 | [" | components[0].components[2].components[2].components[2].FFT.min", 0, 3] [" | components[0].components[2].components[2].components[2].FFT.max", 100, 50] |
麻省理工学院执照;
下面是一个简单的带有lodash深度差分检查器的字体脚本,它将生成一个新的对象,只是旧对象和新对象之间的差异。
例如,如果我们有:
1 2 | const oldData = {a: 1, b: 2}; const newData = {a: 1, b: 3}; |
结果对象为:
1 | const result: {b: 3}; |
它还与多层深度对象兼容,对于数组,可能需要进行一些调整。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 2 3 4 5 6 7 8 9 10 11 | 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); }; |
这是基于@jlavoie,使用lodash
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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://jsfidle.net/emilianobarza/0g0sn3b9/8/