When we test for equality among Javascript primitives, like strings and numbers, we have a couple of quick and easy solutions. we can use our equality operators ("===", "==", and Object.is) and quickly determine if two variables are equal to each other. When we try and do this with functions and objects these solutions fall apart. We can't simply use the equality operators as is.
Why is that?
We can think of every primitive value we create as just linking to an ever-existing value. What do we mean by this?
let deGrom = 48
let rosario = 1
let valentine = 1
We can link our variables to primitives. We can imagine all Javascript primitive values permanently exist and we are simply connecting the variable, to the value.
So, if I attempt to do an equality check…
console.log(rosario === valentine)
// true
We can confirm they are equal!
Upon creation, objects and functions do not point to permanently existing values like primitives. They always create unique values.
console.log({} === {})
// false
console.log(
function redSox(){ return 'dirty cheaters' } ===
function astros(){ return 'dirty cheaters' }
)
// false
Here we have created two new objects and two new functions. Because they are unique, they won't be equal to each other. Even if we define objects or functions that hold the same exact properties, they can't be equal to each other. We would have to make sure our variables reference the same object for the value of each variable to be equal to each other.
// Objects
let team = {valentine: 1, rosario: 1, deGrom: 48}
let teamLink = c
console.log(Object.is(team, teamLink))
// true
// Functions
let cheaters = function astros(){ return 'dirty cheaters' }
let cheatersLink = cheaters
console.log(Object.is(cheaters, cheatersLink))
// true
So how can we compare objects?
We have a couple of options available to us.
Stringify!
One way is to use JSON.stringify() to compare values.
let somePlayers1 =
JSON.stringify({
valentine: 1,
deGrom: 48,
rosario: 1
})
let somePlayers2 =
JSON.stringify({
valentine: 1,
deGrom: 48,
rosario: 1
})
console.log(
somePlayers1 = somePlayers2
)
// true
It worked! Let's try a similar example.
let somePlayers1 =
JSON.stringify({
valentine: 1,
rosario: 1,
deGrom: 48
})
let somePlayers2 =
JSON.stringify({
valentine: 1,
deGrom: 48,
rosario: 1
})
console.log(
somePlayers1 = somePlayers2
)
// false
But wait! That didn't work. Why not?
Order is not guaranteed among objects, so it is important to sort them before attempting a stringify comparison
When comparing strings, they must mirror each other exactly.
One way is to sort our objects keys alphabetically by using Object.keys
let somePlayers1 =
Object.keys({valentine: 1, rosario: 1, deGrom: 48}).sort()
let somePlayers2 =
Object.keys({valentine: 1, deGrom: 48, rosario: 1}).sort()
console.log(
JSON.stringify(somePlayers1) === JSON.stringify(somePlayers2)
)
// true
By using JSON.stringify we've serialized our object into a string, a primitive. Now the two variables somePlayers1 and somePlayers2 both equal {deGrom: 48, rosario: 1, valentine: 1}. We can now compare these two values using equality operators.
Stringify-ing our object is not the most performant method, but it does work.
What happens when we have deeply nested objects though? We would need to perform the same steps we did above for each level of our object.
function sortAllKeys(o){
if (typeof o !== 'object' || !o) {
return o
} else {
return Object.keys(o)
.sort()
.reduce((c, key) => (c[key] = sortAllKeys(o[key])), {})
}
}
Here we are recursively calling our sortAllKeys function. When we finally recurse to the point where our keys point to Javascript primitives and are sorted, we are going to do our type check.
Our if statement will only return true when our keys stop pointing to nested objects. When o evaluates to being a primitive there is no need to recurse anymore and as we pop calls off the stack we can eventually return our deeply nested, sorted object.
let somePlayers1 = {
valentine: {
number: 1
},
rosario: {
number: 1,
isHeGood: true
},
deGrom: {
number: 48,
isHeGood: true
}
}
let somePlayers2 = {
valentine: {
number: 1
},
deGrom: {
number: 48,
isHeGood: true
},
rosario: {
isHeGood: true,
number: 1
}
}
console.log(
JSON.stringify(sortAllKeys(deGrom)) ===
JSON.stringify(sortAllKeys(scherzer))
)
//true
We can use a method like this to compare deeply nested objects, but I think this hints at a deeper problem with trying to use just JSON.stringify.
Outside Library (underscore/lodash)
Using an outside library is probably the simplest, easiest, and quickest option we have. Prior to ES6, lodash and underscore provided many Array and Object methods that didn't natively exist in Javascript. This solved a lot of problems. Instead of creating new methods, you would be provided with tested, production ready methods. Why create something that already exists? Tasks like cloning objects, array flattening and object equality (hey that's us!) are as simple as adding the library to your project.
As an example, lodash provides us with an isEqual which according to lodash documentation, "Performs a deep comparison between two values to determine if they are equivalent."
import isEqual from 'lodash.isequal'
let deGrom = {
position: "pitcher",
ace: true,
organization: {
name: "MLB",
league: "National"
}
}
let scherzer = {
position: "pitcher",
ace: true,
organization: {
league: "National",
name: "MLB"
}
}
console.log(isEqual(deGrom, scherzer))
// true
Although deGrom and scherzer, two aces for their respective teams, are each variables holding objects that look the same, they are different objects and individually created.
Using the isEqual method from lodash, when we compare the two variables we get true.
As an added bonus once these methods find a key/value pair that is not found on the other comparison object, they'll return false. Stringify has to serialize both objects before determining whether they are equal or not.
Creating Your Own Method
This is a fun one if you wanna get a deeper understanding of object equality and how different libraries and programmers try to implement it. We've seen that a combination of recursion, sorting, and checking whether a key/value pair is primitive or object is one route.
Looking at lodash or underscore's implementations a piecing through the code is helpful, and can help solidify understanding just how to implement an algorithm to check object equality.
I would love to see more ways to compare objects below, and...
Let's Go Mets!
Thank you to Dan Abramov's Just Javascript series for helping to solidify primitive and object comparison concepts
Top comments (0)