Deep Equality
Prompt
Write a JavaScript function deepEqual(obj1, obj2) that checks if two given objects (or arrays) are deeply equal.
Playground
Start with the base cases. If both values are the same
reference (===), return true. If either is not an
object or is null, compare them with === and return
the result.
For the recursive case: get the keys of both objects. If
they have different numbers of keys, return false. Then
loop through each key and recursively call deepEqual on
the values. If any pair isn't deeply equal, return
false.
This works for arrays too because Object.keys([1, 2, 3])
returns ['0', '1', '2'] and array values can be accessed
with bracket notation just like objects.
Solution
Explanation
If you've solved shallow equality, you know its limitation: it can't compare nested objects. Two objects that look identical get reported as "not equal" because the nested objects are different references in memory.
shallowEqual(
{ name: 'Alice', address: { city: 'NY' } },
{ name: 'Alice', address: { city: 'NY' } }
);
// false! The two "address" objects are different referencesDeep equality fixes this. Instead of giving up when it hits a nested object, it goes inside that object and compares its contents too. And if those contents have nested objects of their own, it goes inside those as well. It keeps going until it reaches primitive values (numbers, strings, booleans, null) that can be compared with ===.
deepEqual(
{ name: 'Alice', address: { city: 'NY' } },
{ name: 'Alice', address: { city: 'NY' } }
);
// true! It looked inside "address" and compared the contentsThe three cases
The function handles values in three stages:
Are they the same thing? If obj1 === obj2, return true immediately. This catches two references to the same object, two identical numbers, two identical strings, both being null, etc. This is also our recursion's exit point for primitives.
if (obj1 === obj2) return true;Is either one not an object? If we got past the === check and either value is null or isn't an object (it's a number, string, or boolean), then they can't be equal. We already know === failed, so they're different.
if (
obj1 === null ||
obj2 === null ||
typeof obj1 !== 'object' ||
typeof obj2 !== 'object'
) {
return false;
}Why check for null separately? Because typeof null is 'object' in JavaScript (a famous language bug from 1995). Without the null check, we'd try to call Object.keys(null) and crash.
Both are objects? Compare their contents. Get the keys of both. If the counts don't match, they can't be equal. Then loop through each key and recursively call deepEqual on the values:
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (
!keys2.includes(key) ||
!deepEqual(obj1[key], obj2[key])
) {
return false;
}
}
return true;How the recursion works
Let's trace through deepEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } }):
- Both are objects with keys
['a', 'b']. Same count. Start comparing. - Key
'a':deepEqual(1, 1). They're the same primitive (1 === 1), returnstrue. - Key
'b':deepEqual({ c: 2 }, { c: 2 }). Both are objects. Recurse.- Inside: keys are
['c']. Same count. - Key
'c':deepEqual(2, 2). Same primitive, returnstrue. - All keys matched, return
true.
- Inside: keys are
- All keys matched at the top level too. Return
true.
The recursion peels off one layer at a time. It keeps going deeper until it hits primitives, which get compared with ===. Then it unwinds back up, and if everything matched at every level, the final answer is true.
Why this works for arrays too
Arrays in JavaScript are just objects with numeric keys. Object.keys([10, 20, 30]) returns ['0', '1', '2']. So our function compares arrays element by element without needing any special array handling:
deepEqual([1, [2, 3]], [1, [2, 3]]);
// Object.keys gives ['0', '1'] for both
// deepEqual(1, 1) → true
// deepEqual([2, 3], [2, 3]) → recurse → trueIt even handles mixed structures like { a: [1, { b: 2 }] } because the recursion doesn't care whether it's looking at an object or an array. It just compares keys and values all the way down.
Shallow vs Deep: when to use which
Shallow equality is faster and works great for flat objects. React uses it internally for React.memo and useMemo dependency checks. If your data is flat or you intentionally use the same references for nested objects, shallow equality is all you need.
Deep equality is for when you need to know if two complex data structures have the exact same content. Think of comparing API responses, configuration objects, or writing test assertions. It's more thorough but also more expensive since it visits every property recursively.
Performance
Deep equality visits every property in both objects
recursively. For very large or deeply nested objects, this
can be slow. In performance-critical code, shallow
equality with well-structured data is usually a better
choice. Libraries like Lodash provide an optimized
_.isEqual for production use.