javascript - elemento - ¿Cómo hacer una comparación profunda entre 2 objetos con lodash?
comparar dos objetos en javascript (17)
solo usando vanilla js
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);
};
Tengo 2 objetos anidados que son diferentes y necesito saber si tienen una diferencia en una de sus propiedades anidadas.
var a = {};
var b = {};
a.prop1 = 2;
a.prop2 = { prop3: 2 };
b.prop1 = 2;
b.prop2 = { prop3: 3 };
El objeto podría ser mucho más complejo con más propiedades anidadas. Pero este es un buen ejemplo. Tengo la opción de usar funciones recursivas o algo con lodash ...
Aquí hay un simple mecanografiado con el comprobador de diferencias profundas Lodash que producirá un nuevo objeto con solo las diferencias entre un objeto antiguo y un nuevo objeto.
Por ejemplo, si tuviéramos:
const oldData = {a: 1, b: 2};
const newData = {a: 1, b: 3};
el objeto resultante sería:
const result: {b: 3};
También es compatible con objetos profundos de varios niveles, para matrices puede necesitar algunos ajustes.
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;
};
Aquí hay una solución concisa:
_.differenceWith(a, b, _.isEqual);
Basado en la respuesta de Adam Boduch , escribí esta función que compara dos objetos en el sentido más profundo posible , devolviendo rutas que tienen valores diferentes, así como las rutas que faltan en uno u otro objeto.
El código no fue escrito con la eficiencia en mente, y las mejoras en ese sentido son bienvenidas, pero aquí está la forma básica:
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;
}
Puede probar el código con este fragmento (se recomienda ejecutar en modo de página completa):
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));
}
#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;
}
<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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="objects">
<section>
<h1>a (first object)</h1>
<div id="a"></div>
</section>
<section>
<h1>b (second object)</h1>
<div id="b"></div>
</section>
<button onClick="do_compare()">compare</button>
<section>
<h1>result</h1>
<div id="result"></div>
</section>
</div>
Como se le preguntó, aquí hay una función de comparación recursiva de objetos. Y un poquito más. Suponiendo que el uso principal de dicha función es la inspección de objetos, tengo algo que decir. La comparación profunda completa es una mala idea cuando algunas diferencias son irrelevantes. Por ejemplo, la comparación ciega profunda en las afirmaciones de TDD hace que las pruebas sean frágiles innecesarias. Por esa razón, me gustaría presentar una diferencia parcial mucho más valiosa. Es un análogo recursivo de una contribución previa a este hilo. Ignora las claves no presentes en un
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));
BDiff permite verificar los valores esperados mientras tolera otras propiedades, que es exactamente lo que desea para la inspección automática . Esto permite construir todo tipo de afirmaciones avanzadas. Por ejemplo:
_.reduce(a, function(result, value, key) {
return b[key] === undefined ? key : []
}, []);
Volviendo a la solución completa. Construir un diferencial tradicional completo con bdiff es trivial:
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] : [])),
[]);
Ejecutar la función anterior en dos objetos complejos generará algo similar a esto:
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);
Finalmente, para tener una idea de cómo difieren los valores, es posible que queramos evaluar directamente () la salida de diferencias. Para eso, necesitamos una versión más fea de bdiff que genere rutas sintácticamente correctas:
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));
};
Eso generará algo similar a esto:
[
" < 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"
]
Licencia MIT;)
Comparación profunda usando una plantilla de propiedades (anidadas) para verificar
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)
Esto funcionará en la consola. Se podría agregar soporte de matriz si fuera necesario
Completando la respuesta de Adam Boduch, esta toma en diferencias en las propiedades
// 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)))));
Este código devuelve un objeto con todas las propiedades que tienen un valor diferente y también valores de ambos objetos. Útil para registrar la diferencia.
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;
}, {});
Para cualquiera que se encuentre con este hilo, aquí hay una solución más completa. Comparará dos objetos y le dará la clave de todas las propiedades que están solo en objeto1 , solo en objeto2 , o están en objeto1 y objeto2 pero tienen valores diferentes :
/*
* 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;
}
Aquí hay un ejemplo de salida:
// 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"]
Si no le importan los objetos anidados y desea omitir lodash, puede sustituir
_.isEqual
por una comparación de valores normales, por ejemplo,
obj1[key] === obj2[key]
.
Para mostrar recursivamente en qué se diferencia un objeto con otro, puede usar _.reduce combinado con _.isEqual y _.isPlainObject . En este caso, puede comparar cómo a es diferente con b o cómo b es diferente con a:
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);
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
Si necesita saber qué propiedades son diferentes, use reduce() :
_.reduce(a, function(result, value, key) {
return _.isEqual(value, b[key]) ?
result : result.concat(key);
}, []);
// → [ "prop2" ]
Si solo necesita una comparación de teclas:
[" | components[0].components[2].components[2].components[2].FFT.min", 0, 3]
[" | components[0].components[2].components[2].components[2].FFT.max", 100, 50]
Sin el uso de lodash / subrayado, he escrito este código y está funcionando bien para mí para una comparación profunda de object1 con object2
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;
}
Tomé una puñalada del código de Adam Boduch para generar una diferencia profunda: esto no se ha probado por completo, pero las piezas están ahí:
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"]
Una solución fácil y elegante es usar
_.isEqual
, que realiza una comparación profunda:
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
Sin embargo, esta solución no muestra qué propiedad es diferente.
esto se basó en , usando lodash
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 }, {}) }
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
}, {})
}