In JavaScript, object comparison depends on intent: === checks reference identity, while value-based checks require shallow or deep comparison. Learn practical comparison strategies, JSON.stringify pitfalls, and when to use utility libraries for reliable deep equality.
How would you compare two objects in JavaScript?
The core idea
Object comparison in JavaScript is not one single operation. You usually choose between three intents:
- Identity comparison: are both variables pointing to the exact same object?
- Shallow value comparison: are top-level keys and values equal?
- Deep value comparison: are nested objects/arrays equal by content?
Most interview mistakes happen when people use === but expect deep value equality.
const a = { id: 1, profile: { city: 'Berlin' } };
const b = { id: 1, profile: { city: 'Berlin' } };
const c = a;
console.log(a === b); // false (different references)
console.log(a === c); // true (same reference)
Approach | What it checks | Best use case | Main risk |
|---|---|---|---|
| Reference identity only | React memoization, state identity checks | Returns false for equal-looking but separate objects |
Shallow compare | Top-level keys + values | Flat config objects, props optimization | Misses differences in nested objects |
Deep compare | Recursive structural equality | Validation, tests, cache keys, diffing | Higher CPU cost and edge-case complexity |
Shallow comparison helper
Use shallow compare when object values are primitives or when nested objects are intentionally compared by reference.
function shallowEqual(obj1, obj2) {
if (obj1 === obj2) return true;
if (!obj1 || !obj2 || typeof obj1 !== 'object' || typeof obj2 !== 'object') return false;
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (!Object.prototype.hasOwnProperty.call(obj2, key)) return false;
if (!Object.is(obj1[key], obj2[key])) return false;
}
return true;
}
Deep comparison helper (handles cycles)
For nested data, deep comparison is safer. A robust helper should handle arrays, dates, and circular references.
function deepEqual(a, b, seen = new WeakMap()) {
if (Object.is(a, b)) return true;
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
return false;
}
if (a.constructor !== b.constructor) return false;
if (a instanceof Date) return a.getTime() === b.getTime();
if (a instanceof RegExp) return String(a) === String(b);
if (seen.get(a) === b) return true;
seen.set(a, b);
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i], seen)) return false;
}
return true;
}
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
if (!deepEqual(a[key], b[key], seen)) return false;
}
return true;
}
const x = { id: 1, meta: { tags: ['js', 'interview'] } };
const y = { id: 1, meta: { tags: ['js', 'interview'] } };
console.log(shallowEqual(x, y)); // false (nested object references differ)
console.log(deepEqual(x, y)); // true (same nested values)
Why JSON.stringify is not a universal solution
The stringify trick is fast to write, but it breaks in real systems:
- Key order can differ between objects with same logical meaning.
undefined, functions, and symbols are dropped.- Circular references throw errors.
Date,Map,Set, and class instances need special handling.
const p = { a: 1, b: undefined };
const q = { a: 1 };
console.log(JSON.stringify(p) === JSON.stringify(q));
// true (but objects are not semantically identical for many use cases)
Production guidance
- Use
===for identity checks (cheap and exact). - Use shallow compare for flat objects and render optimization.
- Use a proven library (for example
fast-deep-equal) when deep equality must be reliable and fast. - In tests, include edge cases:
NaN, dates, arrays, and circular references.
Practical scenario
A product filter panel compares previous and next query objects to decide whether to refetch results. A strict reference check misses equal-by-value objects, while naive deep checks hurt performance on every keystroke.
Common pitfalls
- Using
===when your intent is value equality. - Using
JSON.stringifyfor complex or cyclic objects. - Running deep comparison on large objects inside hot render loops.
Choose comparison depth by use case, then benchmark. In UI code, prefer stable object creation plus targeted shallow checks; in validation/tests, use robust deep equality.
Use the relevant interview-question hub first, then move into a concrete study plan before targeted company sets.