frontend
Lodash isEqual() Implementation in JavaScript
January 24, 2026
Lodash isEqual() Implementation in JavaScript
Overview
Lodash's isEqual() performs a deep equality check between two values. Unlike === or ==, it recursively compares objects and arrays, checking if they have the same structure and values, regardless of reference equality.
Basic Implementation
/**
* lodash isEqual()
* Simpler version for interview purpose.
*/
function simplerIsEqual(a, b) {
if (a === b) {
return true;
}
if (
a === null ||
b === null ||
typeof a !== "object" ||
typeof b !== "object"
) {
return false;
}
if (Object.keys(a).length !== Object.keys(b).length) {
return false;
}
for (let key in a) {
if (!simplerIsEqual(a[key], b[key])) return false;
}
return true;
}
Complete Implementation
/** Supports data types like:
* Primitive data types
* Array, Objects literals, Function
*/
function isEqual(obj1, obj2) {
function getType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
let type = getType(obj1);
// If the two items are not the same type, return false
if (type !== getType(obj2)) return false;
if (type === "array") return areArraysEqual();
if (type === "object") return areObjectsEqual();
if (type === "function") return areFunctionsEqual();
function areArraysEqual() {
// Check length
if (obj1.length !== obj2.length) return false;
// Check each item in the array
for (let i = 0; i < obj1.length; i++) {
if (!isEqual(obj1[i], obj2[i])) return false;
}
// If no errors, return true
return true;
}
function areObjectsEqual() {
if (Object.keys(obj1).length !== Object.keys(obj2).length) return false;
// Check each item in the object
for (let key in obj1) {
if (Object.prototype.hasOwnProperty.call(obj1, key)) {
if (!isEqual(obj1[key], obj2[key])) return false;
}
}
// If no errors, return true
return true;
}
function areFunctionsEqual() {
return obj1.toString() === obj2.toString();
}
function arePrimativesEqual() {
return obj1 === obj2;
}
return arePrimativesEqual();
}
// Usage
const a = {
name: "Ashish",
age: 24,
more: {
random: "data",
},
};
const b = {
name: "Ashish",
age: 24,
more: {
random: "data",
},
};
const res = simplerIsEqual(a, b);
console.log(res); // true
Enhanced Implementation
Handling More Data Types
function isEqual(a, b) {
// Same reference
if (a === b) {
return true;
}
// Handle null
if (a === null || b === null) {
return a === b;
}
// Handle undefined
if (a === undefined || b === undefined) {
return a === b;
}
// Different types
if (typeof a !== typeof b) {
return false;
}
// Handle primitives
if (typeof a !== "object") {
return a === b;
}
// Handle Date
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
}
// Handle RegExp
if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source && a.flags === b.flags;
}
// Handle Array
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!isEqual(a[i], b[i])) return false;
}
return true;
}
// Handle Object
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) {
return false;
}
for (let key of keysA) {
if (!keysB.includes(key)) {
return false;
}
if (!isEqual(a[key], b[key])) {
return false;
}
}
return true;
}
With Circular Reference Handling
function isEqual(a, b, visited = new WeakMap()) {
// Same reference
if (a === b) {
return true;
}
// Handle null/undefined
if (a == null || b == null) {
return a === b;
}
// Different types
if (typeof a !== typeof b) {
return false;
}
// Handle primitives
if (typeof a !== "object") {
return a === b;
}
// Check for circular references
if (visited.has(a) && visited.get(a) === b) {
return true;
}
visited.set(a, b);
// Handle Date
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
}
// Handle RegExp
if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source && a.flags === b.flags;
}
// Handle Array
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!isEqual(a[i], b[i], visited)) return false;
}
return true;
}
// Handle Object
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) {
return false;
}
for (let key of keysA) {
if (!keysB.includes(key)) {
return false;
}
if (!isEqual(a[key], b[key], visited)) {
return false;
}
}
return true;
}
Use Cases
1. State Comparison
const previousState = { user: { name: "John" } };
const currentState = { user: { name: "John" } };
if (isEqual(previousState, currentState)) {
console.log("State unchanged");
}
2. Form Validation
const originalData = { name: "John", age: 30 };
const editedData = { name: "John", age: 30 };
if (isEqual(originalData, editedData)) {
console.log("No changes made");
} else {
console.log("Form has been modified");
}
3. Cache Invalidation
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
for (const [cachedKey, cachedValue] of cache.entries()) {
if (isEqual(JSON.parse(cachedKey), args)) {
return cachedValue;
}
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
4. Testing
function assertEqual(actual, expected) {
if (!isEqual(actual, expected)) {
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
}
}
Comparison with Other Methods
=== (Strict Equality)
const a = { x: 1 };
const b = { x: 1 };
a === b; // false (different references)
isEqual(a, b); // true (same structure)
== (Loose Equality)
1 == "1"; // true (type coercion)
isEqual(1, "1"); // false (different types)
JSON.stringify
// ❌ Doesn't handle functions, undefined, symbols
JSON.stringify({ a: undefined }) === JSON.stringify({ b: undefined }); // true (wrong!)
// ✅ Handles all types
isEqual({ a: undefined }, { b: undefined }); // false (correct)
Performance Considerations
Optimized Version
function isEqualFast(a, b) {
if (a === b) return true;
if (a == null || b == null) return a === b;
if (typeof a !== "object" || typeof b !== "object") return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
if (!keysB.includes(key) || !isEqualFast(a[key], b[key])) {
return false;
}
}
return true;
}
Best Practices
- Use for Deep Comparison: When you need structural equality
- Handle Edge Cases: null, undefined, dates, regex
- Consider Performance: Can be slow for large objects
- Use for Testing: Perfect for assertion libraries
- Cache Results: For frequently compared objects
- Type Safety: Ensure types match before deep comparison
Common Pitfalls
Pitfall 1: Function Comparison
// Functions are compared by string representation
const fn1 = () => console.log("test");
const fn2 = () => console.log("test");
isEqual(fn1, fn2); // true (same string)
Pitfall 2: Property Order
// Property order doesn't matter
const a = { x: 1, y: 2 };
const b = { y: 2, x: 1 };
isEqual(a, b); // true
Pitfall 3: Prototype Properties
// Only own properties are compared
class MyClass {}
const a = new MyClass();
const b = new MyClass();
isEqual(a, b); // true (if no own properties)
Real-World Example
class StateManager {
constructor() {
this.state = {};
this.listeners = [];
}
setState(newState) {
if (!isEqual(this.state, newState)) {
this.state = newState;
this.notifyListeners();
}
}
notifyListeners() {
this.listeners.forEach(listener => listener(this.state));
}
}