Checking if two JavaScript objects are equal to see if you need to update the UI is a common task in React. (Ideally you wouldn’t mutate your objects, but oh well.)
This post compares Node’s deepEqual with a JSON.stringify
based comparison in terms of performance and behavior.
Warning: JSON.stringify may return different strings for two deep equal objects
If the two objects are created in different ways the order of the keys will be different:
JSON.stringify({a: 1, b: 2}) !== JSON.stringify({b: 2, a: 1})
`{"a":1,"b":2}` !== `{"b":2,"a":1}`
Also note that the JavaScript does not guarantee for the keys to be in a particular order, though in practice the order is consistent.
Reference equality, shallow equality and deep equality
First of all, let’s look at why we need a deep comparison instead of just using ===
.
In this example I’m using the shallowEquals and deepEqual libraries. Since deep-equal
on NPM requires a module loader I’ve used the bower version instead.
user1 = {
name: "John",
address: {
line1: "55 Green Park Road",
line2: "Purple Valley"
}
}
This is the object we are going to compare against.
user2 = user1;
console.log("user1 === user2", user1 === user2);
console.log("shallowEqual(user1, user2)", shallowEqual(user1, user2));
console.log("deepEqual(user1, user2)", deepEqual(user1, user2));
// user1 === user2 true
// shallowEqual(user1, user2) true
// deepEqual(user1, user2) true
Since both user1
and user2
ultimately refer to the same object all our comparisons return true.
However, there are many cases where two objects have the same content but don’t reference the same object.
user2 = {
name: "John",
address: user1.address
}
console.log("user1 === user2", user1 === user2);
console.log("shallowEqual(user1, user2)", shallowEqual(user1, user2));
console.log("deepEqual(user1, user2)", deepEqual(user1, user2));
// user1 === user2 false
// shallowEqual(user1, user2) true
// deepEqual(user1, user2) true
Here the objects have the same data but are not referentially equal.
Finally, let’s not directly re-use any references from user1
and just re-type the same object literal.
user2 = {
name: "John",
address: {
line1: "55 Green Park Road",
line2: "Purple Valley"
}
}
console.log("user1 === user2", user1 === user2);
console.log("shallowEqual(user1, user2)", shallowEqual(user1, user2));
console.log("deepEqual(user1, user2)", deepEqual(user1, user2));
// user1 === user2 false
// shallowEqual(user1, user2) false
// deepEqual(user1, user2) true
shallowEqual
works by comparing each object property of the two users using ===
. That means that when it reaches the address
object, it doesn’t go deeper to compare the contents and relies on the two objects having the same reference.
As a result shallowEqual
thinks the two objects are equal in the second example but not in the third.
deepEqual
on the other hand goes deeper into the object when it reaches the address. It compares the strings for line1
and line2
with ===
and decides that the two objects are equal.
Checking for equality with JSON.stringify
Another way to compare two objects is to convert them to JSON and check if the resulting strings are equal:
function jsonEqual(a,b) {
return JSON.stringify(a) === JSON.stringify(b);
}
jsonEqual(user1, user2) // true in all three cases above
Like deepEqual
this method cares about the contents of the object, rather than referential equality.
Comparing the performance of JSON.stringify and deepEqual
I created a performance comparison between jsonEqual
and deepEqual
.
This was the result:
What a surprise! The reason I was using deepEqual
was that I thought jsonEqual
would be an order of magnitude slower. Turns out it’s actually faster!
Caveats to the performance comparison
However, on the whole it’s still much better to use deepEqual
. (It’s only 42% slower anyway.)
There are two cases where deepEqual
is faster than a JSON comparison.
-
if the two objects are referentially equal
jsonEqual
will still generate the two complete JSON strings.deepEqual
on the other hand will immediately see that the two options are the same and finish 8000 times more quickly. -
While
jsonEqual
is faster at confirming that two objects are equal,deepEqual
is much faster at finding out that they aren’t. As soon as it finds two properties that don’t match up it returnsfalse
, rather than continuing to look for differences.
Differences in behavior between the two methods
A big problem with jsonEqual
is that it can report false negatives. For example it would say that these two objects aren’t equal:
jsonEqual({a: 1, b: 2}, {b: 2, a: 1}); // false
Deep equal is much more robust and doesn’t rely on the ordering of the properties.
Another difference between the two methods is that JSON.stringify
does not serialize functions.
jsonEqual({a: 5, b: function(){}}, {a: 5}); // true
deepEqual
will instead check for reference equality between a function and its counterpart in the other object.