jfriend00's function is a good start, but isn't very descriptive when the issue is a ways down the object model, as instead of acting recursively, it punts when both objects have the same properties pointing to objects. Instead of walking the object model, it falls back using angular.equals
.
That is, jfriend's solution only defers angular.equals
limitations one level of the object hierarchy.
For instance, it fails mightily on this test:
var obj1 = {
a: 1,
b: {
c: 2,
d: {
e: 3,
f: 4,
},
g: 5,
},
h: 6,
};
var obj2 = {
a: 1,
b: {
c: 2,
d: {
e2: 3, // DIFFERENT PROPERTY
f: 'different', // DIFFERENT TYPE AND VALUE
},
g: 'again', // DIFFERENT TYPE AND VALUE
},
h: 7, // DIFFERENT VALUE
};
It tells us only:
- property b does not match
- property h does not match
That's not wrong, but it's also not particularly descriptive, and didn't help me find differences between two complex objects when angular.equals
came up false.
A better solution
We can improve on that.
If we wanted to know specifically that [obj].b.d.f
are different values, we'd need to recurse down the object model. We'd also need to recurse down both object models to see when obj2
has something obj1
did not. (To do that efficiently, a different function is used for the second comparison.)
Here's a cut that does that, I believe:
// The unicorn: https://stackoverflow.com/a/18234317/1028230
// prettier-ignore
String.prototype.formatUnicorn = String.prototype.formatUnicorn || function () {
var str = this.toString(); if (arguments.length) { var t = typeof arguments[0]; var key; var args = 'string' === t || 'number' === t ? Array.prototype.slice.call(arguments) : arguments[0]; for (key in args) { str = str.replace(new RegExp('\\{' + key + '\\}', 'gi'), args[key]); } }
return str;
};
function compareObjects(s, t, propsToIgnore1) {
return compareFirstToSecond(s, t, propsToIgnore1, '/').concat(
compareSecondToFirst(t, s, propsToIgnore1, '/')
);
function compareFirstToSecond(first, second, propsToIgnore, path) {
var msgs = [];
propsToIgnore = propsToIgnore || [];
// Check type
if (typeof first !== typeof second) {
msgs.push(
path +
' -- two objects not the same type $${0}$${1}$$'.formatUnicorn(
typeof first,
typeof second
)
);
return msgs;
}
// Check value
// Keep in mind that typeof null is 'object'
// https://stackoverflow.com/a/18808270/1028230
if (typeof first !== 'object' || first === null) {
if (first !== second) {
msgs.push(
'{2} -- Unequal (null and not null) or (two unequal non-objects): {0}-{1} '.formatUnicorn(
first,
second,
path
)
);
}
return msgs;
}
// Check properties
for (var prop in first) {
if (propsToIgnore.indexOf(prop) === -1) {
if (first.hasOwnProperty(prop) && first[prop] !== undefined) {
if (second.hasOwnProperty(prop) && second[prop] !== undefined) {
msgs = msgs.concat(
compareFirstToSecond(
first[prop],
second[prop],
propsToIgnore,
path + prop + '/'
)
);
} else {
msgs.push(path + prop + ' -- second object does not have property ' + prop);
}
}
}
}
return msgs;
}
// now verify that t doesn't have any properties
// that are missing from s.
// To recurse this properly, let's set up another function.
function compareSecondToFirst(second, first, propsToIgnore, path) {
var msgs = [];
propsToIgnore = propsToIgnore || [];
for (var prop in second) {
if (propsToIgnore.indexOf(prop) === -1) {
if (second.hasOwnProperty(prop) && second[prop] !== undefined) {
if (!first.hasOwnProperty(prop) || first[prop] === undefined) {
msgs.push(path + prop + ' -- first object does not have property ' + prop);
} else if (
typeof first[prop] === 'object' &&
typeof second[prop] === 'object'
) {
// NOTE: To make this proceed down the object tree, even though we've already
// checked equality for each match, we need to keep recursively calling
// a check to see if the second object's object model has a prop the first's
// model does not.
//
// That is, we don't know what "props of props" are missing all the way
// down the object model without this call. But we're recursively calling this
// inner function so we don't do any extra comparision work.
msgs = msgs.concat(
compareSecondToFirst(
second[prop],
first[prop],
propsToIgnore,
path + prop + '/'
)
);
} // else they aren't objects and we already know the props values match, as they've already been checked.
}
}
}
return msgs;
}
}
This will tell us not just what's different but the path down the object model to where each difference lives. It also has something that's important to me: The ability to skip properties with the propsToIgnore
array parameter. Your use case may differ, of course.
- /b/d/e -- second object does not have property e
- /b/d/f/ -- two objects not the same type $$number$$string$$
- /b/g/ -- two objects not the same type $$number$$string$$
- /h/ -- Unequal (null and not null) or (two unequal non-objects): 6-7
- /b/d/e2 -- first object does not have property e2
That's getting closer to helping us know what's different, I believe. As you can imagine, as the objects become more complex, having this drill down reporting becomes more useful.
Here's the requisite fiddle of reasonable success with the two compareObjects
methods and results:
https://jsfiddle.net/t4oh7qu9/1/