A Guide To Object Based Comparison In JavaScript
I was reading javascript grammar by JavaScript Teacher, when i suddenly came across comparing objects in one of the chapters. Some years back when i i was still learning some basic javascript, i found it difficult to wrap my head around primitive data types and objects. It just didn’t make sense to me why comparison operators didn’t work with objects.
Reading about this same problem in javascript grammar brought the problem back to memory and inspired me to write a basic boilerplate code that helps with comparing both simple and complex objects by using a recursive function.
The project is completely open source and can be found here on my github repo. It has also been published as an npm module and can be found here.
This article is focused mainly on explaining why comparison operators don’t work with objects and shows a step to step guide to the object comparison module i wrote.
Everything in JavaScript can be represented as an object including its primitive data types. When we want to check for equality between two data types, we simply use comparison operators on this two data types.
let a = 1
let b = 1console.log(a === b) //returns true for a primitive valuelet objA = {}
let objB = {}
let objC = objAconsole.log(objA === objB) //returns false
console.log(ObjA === objC) //returns true
As shown above, comparing two different numbers of the same value will evaluate to true as expected but why does comparing two Objects return false in the first instance and true in the last instance even when they are clearly the same?
Primitive data’s are passed by value, which means a copy of the primitive itself is passed while objects are passed by their reference. So the same object stored in two different variables have different references and hence evaluates to false. So how do we compare two Objects even though they have completely different references?
A very simple approach to this would be to convert the objects into strings using JSON.stringify(object)
and compare the strings to each other, but then another problem arises.
let a = {
name: 'felix',
age: 30,
weight: '45kg'
}let b = {
name: 'felix',
age: 30,
weight: '45kg'
}let c = {
age: 30,
weight: '45kg',
name: 'felix'
}console.log(JSON.stringify(a) === jSON.stringify(b)) //returns trueconsole.log(JSON.stringify(a) === JSON.stringify(c)) //returns false
In the above code example, we would notice that variables a, b and c
are exactly the same objects although c
is structured differently from the rest but still has the exact same key-value pair as the rest. But because c
has been re-ordered, it evaluates to false. Even when not re-ordered, it would still evaluate to false if your object contains nodelists and htmlcollections.
So how do we go about this? Lets first write a simple function that takes in two objects as an argument . The function should return true if this objects are equal and throw an error when something else goes wrong.
function compareObjects(obj1, obj2){
return true;
}
Before we proceed, we wouldn’t want to keep doing repetitive tasks like throwing an error every time something goes wrong. So lets write a simple function to handle that for us.
function reject(msg) {
throw new Error(msg);
}
So before we go ahead to do anything, we would want to make sure we are really working with two objects right? So let’s write a simple script that does just that for us!
function compareObjects(obj1, obj2){
//first confirm if both parameters passed to the arguments are objects
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {reject("Parameters passed into the arguments must be Objects")
}return true;
}
Good, we can now ascertain that the two parameters passed in are objects but what if they where both created from a different constructor? What if one was a Date object and the other an Array? They are both still objects and should pass the initial check right? We want to make sure only objects of the same same constructor make it to the next phase. we could do this as such
//Check if the Objects have different constructors
if (obj1.constructor !== obj2.constructor) {
reject("Objects have different constructors!");
}
Great! we are now sure that both parameters passed to the arguments are objects and of the same constructors.
The next thing we’d want to do is to get all the keys of both objects in form of an array, so we could make sure each of this object has the same keys, and values.
let objKeys1 = Object.keys(obj1).sort();
let objKeys2 = Object.keys(obj2).sort();
You would immediately notice that the keys of both objects has been sorted using the Array.prototype.sort method.
This is done to to aid uniformity between the key Array of both objects so they are both arranged in the same order. Before proceeding, we would want to confirm if both objects have the exact same number of keys, right?
//If the keys are of different lengths
if (objKeys1.length !== objKeys2.length) {
reject("Ooops!, the objects passed in are structured differently")
}
Since we are now sure that each object have exactly the same number of keys, we would want to do a couple of things.
- Loop through each object and check if each of this keys are equal
- If they are, we would want to check the type of each object values and compare them with each other.
for (let i in objKeys1) {
//check if both object have the same keys arranged in the same order
if (objKeys1[i] !== objKeys2[i]) {
reject("Ooops!, the objects passed in are structured differently. Their keys mismatched!")}else{
//handle each type of supported values differently
const value2 = obj2[objKeys1[i]];
const value1 = obj1[objKeys1[i]];
// handle different types of objects
}
}
Different types of objects are compared differently, the way Sets are compared is not the same was Arrays are compared. The next thing we would want to do is to declare a function valueHandler
that compares each object based on their type.
function valueHandler()
checks for the type of the object passed in using a switch statement and handles the comparison appropriately. The comparison is pretty much straight forward for numbers booleans and strings.
For functions, it calls both function and compares the values they return since there is no way to really inspect what goes on internally within the function.
It becomes very interesting when the type of value in an object is also an object, this is because the object could be basically anything. It could be an array, a date object, a regular expression or even a set.
To know precisely what type of object we are dealing with, we first check for the instanceof
the object. If the instance is a regualr expresssion, we simply convert both objects to a string and compare them.
If it where a date, we simply compare the .valueOf
this date objects while for sets we check if they are of the same size and check if each element in the first set has that same element in the second set.
Things become a bit more interesting, when the value is an array. An array could contain any type of object. It could contain another array, an object literal, a date, a regular expression, boolean, string, number, sets etc.
So how do we handle all of this data? By re-inventing the wheel! we simply call the valueHandler()
function again. A function calling itself? Oh yeah, recursion!! The valueHandler()
function handles both primitive data types and objects and hence can be used to compare any type of object in the arrays.
What if the value is an object literal? we initially wrote the compareObjects()
function for object literals, but now we have an object literal inside an object literal which simply means we can call compareObjects()
recursively.
The entire codebase is available here on github.
Testing the code
The project has been published as npm package.
Installation
npm install compare-object --save
After installing the module, you can require it as shown
const objCompare = require('compare-object');
If we have a basic object a and be that contains several types of data to be compared as shown
const a = {
a: 1,
b: 'string',
c: true,
d: () => 'wooow!!',
e: new Date(15, 7, 2019),
f: new RegExp('Gbadebo', 'gi'),
g: new Set([1, 2, 3, 4]),h: [1, 2, 3, true, "gbadebo", new Date(15, 7, 2019), new Set([4, 5, 6, 7])]const b = {
f: new RegExp('Gbadebo', 'gi'),
c: true,e: new Date(15, 7, 2019),
h: [1, 2, 3, true, "gbadebo", new Date(15, 7, 2019), new Set([4, 5, 6, 7])],
b: 'string'
g: new Set([1, 2, 3, 4]),d: () => 'wooow!!',
a: 1}console.log(objCompare(a, b)) //returns true
We would immediately notice that even though object b
has been re-arranged. It’s still pretty much the same as object a
.
Let’s try it with a more complex object and note that it still works quite well
if we run objCompare(obj1, obj2)
It returns true as expected since both obj1 and obj2 are the same but structured differently.
Conclusion
There is no simple rule of thumb to how two objects can be compared, it mostly depends on personal preference and the approach which was applied.
Thanks for reading!
Cheers!!!