How Lodash isEqual Prevents Recursion Limits
Lodash's _.isEqual performs deep comparisons between
values while actively guarding against infinite recursion and call stack
exhaustion. To prevent recursion limits from being reached, Lodash
relies on an internal stack-based cache to detect circular references,
fast-path reference checks, structural early-exit validations, and an
adaptive memoization structure that keeps deep object traversals
performant and safe.
Circular Reference Detection via an Internal Stack
The primary mechanism for preventing infinite recursive loops in
_.isEqual is the internal Stack class (found
in _baseIsEqualDeep.js and _Stack.js). When
evaluating nested objects, Lodash maintains an execution stack
representing the ancestry of currently traversed structures:
- Stack Association: Before recursing into nested
properties of two objects,
baseIsEqualDeeprecords the current pair in the stack viastack.set(objA, objB). - Lookup Prior to Traversal: During recursive steps,
Lodash queries the stack using
stack.get(objA). IfobjAhas already been recorded and matchesobjB, it indicates a circular reference that has already been resolved or is currently in progress. - Loop Short-Circuiting: Upon finding a matching pair
in the stack, Lodash bypasses further recursion and returns
true, effectively breaking reference loops that would otherwise trigger aRangeError: Maximum call stack size exceeded.
Fast-Path Identity Checks
Before invoking deep traversal logic, Lodash executes strict identity
checks via baseIsEqual. If value === other,
Lodash immediately returns true (with a special case
adjustment for +0 and -0, as well as
NaN).
For objects sharing the same memory reference, this check returns immediately, preventing the engine from iterating over child keys or allocating comparison stacks.
Structural Pre-Checks and Early Exits
Recursion depth is significantly reduced through heuristics executed before traversing nested properties:
- Tag and Type Checks: Lodash inspects internal
Object.prototype.toStringtags usinggetTag. If two objects possess incompatible internal types (such as comparing anArrayto aSet, or aDateto aRegExp), deep evaluation terminates before descending further. - Key Length Comparison: For plain objects and
arrays, Lodash checks whether
Object.keys(objA).length === Object.keys(objB).length. If the key counts differ, comparison halts immediately without recursing through the elements. - Property Existence Verification: Lodash verifies
key ownership via
hasOwnPropertybefore deeply comparing property values. If a key is missing on either object, the branch is discarded.
Adaptive
Stack Architecture (ListCache to
MapCache)
To ensure tracking overhead does not degrade performance or exhaust
memory during deep evaluations, the internal Stack
dynamically scales its internal data structure:
- For shallow comparisons with fewer than 200 elements, it uses a
lightweight array-based
ListCache, minimizing allocation overhead. - When comparisons exceed 200 elements, it automatically upgrades to a
hash-based
MapCache(utilizing nativeMapwhere available). This preserves \(O(1)\) lookups for circular reference checks, ensuring that deep and wide structures do not stall JavaScript's single-threaded event loop.