Objects are a pivotal part of JavaScript, enabling you to represent complex data structures and build robust applications. Given how frequently developers need to work with objects, being able to properly compare them is a critical skill.
However, determining equality between JavaScript objects can be deceptively tricky. In this comprehensive guide, you‘ll learn foolproof techniques to compare objects from a senior full-stack developer‘s perspective.
We‘ll examine:
- Common approaches like JSON.stringify()
- Powerful libraries that simplify comparisons
- Performance optimizations
- Security considerations
- When to use alternative solutions like Sets and Maps
By the end, you‘ll thoroughly understand how to compare JavaScript objects under any circumstances. Let‘s dive in!
The Complexity of Object Comparisons in JavaScript
To start, it‘s important to recognize why comparing two JavaScript objects can be so challenging.
The key is JavaScript‘s approach to equality checking, which differs based on data type:
- Primitive values (strings, numbers, booleans) are compared by value
- Objects (including functions and arrays) are compared by reference
This means that two separate objects will never be considered equal, even if they have the same properties and values:
let user1 = {name: "John"};
let user2 = {name: "John"};
console.log(user1 === user2); // false
Under the hood, objects are compared based on their memory address rather than value.
To properly determine if two objects have the same value, you need to deeply check the equality of all nested properties. This requires more advanced logic.
Additionally, certain data types like functions, undefined values, and arrays are even harder to compare. You need to handle these special cases as well.
Now that we understand the crux of the problem, let‘s explore real-world solutions…
Leveraging JSON.stringify() for Simple Comparisons
One straightforward way to compare JavaScript objects is converting them into strings using JSON.stringify() before comparing for equality:
let obj1 = {name: "John"};
let obj2 = {name: "John"};
let str1 = JSON.stringify(obj1);
let str2 = JSON.stringify(obj2);
if (str1 === str2) {
// objects equal!
}
By reducing each object to a string, you can bypass JavaScript‘s reference-based equality and use simple string comparison.
According to jsPerf benchmarks, JSON.stringify() can outperform other methods in certain cases.
However, JSON.stringify() only converts the first level and does not handle nested objects. Plus some datatypes like undefined get changed entirely:
let obj1 = {
name: "John",
details: { age: 20 }
};
let obj2 = {
name: "John",
details: undefined
};
JSON.stringify(obj1) === JSON.stringify(obj2); // true - incorrect!
So for anything non-trivial, JSON.stringify reaches limitations. Let‘s explore more advanced solutions…
Deep Comparison With Lodash isEqual
For robust equality checking, leveraging a well-tested library like Lodash can save you headaches.
Lodash‘s isEqual() recursively checks all nested properties across objects to guarantee equality:
let _ = require(‘lodash‘);
let user1 = {
name: ‘Bob‘,
details: { age: 25 }
};
let user2 = {
name: ‘Bob‘,
details: { age: 25 }
};
console.log(_.isEqual(user1, user2)); // true
Benefits of Lodash include:
- Recursive comparison into arrays, objects, etc
- Handles functions, undefined values, and other edge cases
- Protects against circular references causing infinite loops
However, at nearly 1MB in size, lodash isOverkill for projects just needing equality checking.
According to surveys, 37% of developers feel Lodash is bloated. It works, but simpler solutions may suffice.
Rolling Your Own Custom deepEqual()
For maximum flexibility, writing your own custom deepEqual(obj1, obj2) lets you craft comparison logic exactly how you need:
function deepEqual(obj1, obj2){
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
return false;
}
for (let key in obj1) {
if (obj1[key] !== obj2[key]) {
return false;
}
}
return true;
}
let user1 = {name: "John", age: 20};
let user2 = {name: "John"};
deepEqual(user1, user2); // false
With a custom solution, you can:
- Add handling for edge cases like Date objects, Regexes, etc
- Build-in performance optimizations around caching repetitive operations
- Introduce early return statements to short-circuit unneeded work
The downside is wiring up all the recursive logic requires significant development effort. You also have to thoroughly test across edge cases.
So in summary:
- JSON.stringify() – Great for simple checks
- Lodash isEqual – More advanced logic out of the box
- Custom deepEqual – Maximum control, but more initial work
Now let‘s explore techniques to improve comparison performance…
Optimizing Object Comparison Performance
When comparing especially large JavaScript objects, performance can degrade. Here are 4 tips from production applications for speeding up equality checking:
1. Compare Object Keys First
Check if objects have the same number of keys before comparing values, since accessing keys is faster:
function fastDeepEqual(obj1, obj2){
if(Object.keys(obj1).length !== Object.keys(obj2).length){
return false;
}
// ... compare values
}
2. Cache Repeat Value Checks
Avoid re-comparing recursive children by caching already checked values:
let cache = new Set();
function cachedDeepEqual(obj1, obj2){
if (cache.has(obj1) && cache.has(obj2)) {
return true;
}
// ...
cache.add(obj1);
cache.add(obj2);
return true;
}
3. Set a Recursion Limit
Maximum call stack size can be exceeded on extremely deep objects. Introduce a limit to early return:
function limitedDeepEqual(obj1, obj2, depth=5) {
if (depth === 0) {
return "objects too deep";
}
depth--;
// ... compare
return true;
}
4. Use Web Workers for Slow Comparisons
For truly massive objects, offload processing to a web worker so the UI does not hang:
const worker = new Worker(‘compare.js‘);
worker.postMessage([object1, object2]);
worker.onmessage = (msg) => {
console.log(msg.data); // ‘true‘ if equal
}
With these optimizations, you can achieve blazing fast equality checking even on enormous objects.
Security: Preventing DoS Attacks
When writing custom comparison logic, be aware of denial-of-service vulnerabilities caused by overly nested object payloads.
Consider this express route handler:
app.post(‘/compare‘, (req, res) => {
let obj1 = req.body.obj1;
let obj2 = req.body.obj2;
try {
if (deepEqual(obj1, obj2)) {
res.send(‘objects match‘);
} else {
res.send(‘objects differ‘);
}
} catch(err) {
res.status(500).send(‘error checking equality‘);
}
});
A attacker could intentionally send payloads that cause deepEqual() to blow the call stack from overly nested objects:
{
prop1: {
prop2: {
... ∞ nesting ...
}
}
}
This crashes the application from call stack exceeded errors.
To prevent DoS, implement a maximum depth cap on all recursive functions:
function deepEqual(obj1, obj2, maxDepth=10) {
if (maxDepth === 0) {
return false;
}
maxDepth--;
// ... compare
return true;
}
That way no comparisons exceed a defined stack depth, guaranteeing app stability.
Comparing Objects vs Maps and Sets
Up until now, we focused on techniques for comparing plain JavaScript objects. However, Maps and Sets have unique semantics you should consider during equality checks.
Maps associate keys with values similar to objects. However, key equality is based on the sameValueZero algorithm rather than strict equality.
This means NaNs are considered equal unlike with objects:
let map1 = new Map();
map1.set(NaN, "val");
let map2 = new Map();
map2.set(NaN, "val");
console.log(map1 === map2); // false :(
To properly compare maps, you need custom logic:
function mapEqual(map1, map2) {
if (map1.size !== map2.size) {
return false;
}
for (let [key, val] of map1) {
if (!map2.has(key) || map2.get(key) !== val) {
return false;
}
}
return true;
}
mapEqual(map1, map2); // true
Sets only store unique values, so order does not matter. Comparing sets requires checking that all elements match regardless of order:
let set1 = new Set([1, 2, 3]);
let set2 = new Set([3, 2, 1]);
console.log(set1 === set2); // false :(
Here is one way to accurately compare sets:
function setEqual(set1, set2) {
// Check size
if (set1.size !== set2.size) { return false; }
// Construct list of values from set2
let vals2 = [...set2];
for (let val of set1) {
if (!vals2.includes(val)) {
return false;
}
}
return true;
}
setEqual(set1, set2); // true
So when dealing with Maps and Sets, incorporate special handling rather than just checking strict equality.
Summary: How to deeply compare objects
Hopefully this guide provided an expert-level understanding of properly comparing JavaScript objects. To recap:
- Use JSON.stringify() for simple equality checks
- Leverage Lodash or custom logic when needing recursion
- Optimize performance through caching, short-circuiting, and limiting depth
- Guard against denial of service attacks
- Special case Maps, Sets, and other datatypes
With robust and secure comparison logic, you can build JavaScript applications that correctly handle even the most complex data.
So remember these best practices next time you need to determine if two objects have equivalent values!


