How Lodash isEqual Performs Deep Comparison
Lodash's _.isEqual method provides deep value comparison
across JavaScript data structures, moving beyond the reference-based
checks of the native strict equality operator (===). To
determine whether two values are functionally and structurally
identical, the utility combines primitive fast-paths, internal type-tag
checks, specialized object unwrapping, recursive traversal, and cycle
detection. This article breaks down the internal algorithms and
heuristics Lodash utilizes to implement robust deep equality.
1. The Fast-Path Primitive Comparison
Before initiating expensive traversal algorithms,
_.isEqual performs a rapid equality check. It relies on the
SameValueZero algorithm, which mirrors ===
with two exceptions: it treats NaN as equal to
NaN, and +0 as equal to -0. If
both references are identical or both values evaluate to
true under this primitive check, the function immediately
returns true.
2. Internal Tag Resolution
When values fail the initial fast-path check, Lodash determines their
underlying JavaScript types using internal type tags obtained via
Object.prototype.toString.call(). This step ensures that
values sharing an apparent structural similarity but differing in
underlying specifications (for instance, an Array versus an
Arguments object) are identified correctly before comparing
values. If the type tags differ, the comparison immediately returns
false.
3. Specialized Value Unwrapping
Lodash defines explicit comparison routines for built-in JavaScript objects based on their internal tag:
- Dates: Compared by their primitive millisecond
timestamps using
+dateA == +dateB. - RegExps: Compared by matching both their pattern
sources (
source) and their attached flags (flags). - Booleans, Numbers, and Strings: Object-wrapped
primitives (e.g.,
new Number(5)) are unwrapped viavalueOf()or coerced to their primitive states before comparison. - ArrayBuffers and TypedArrays: Validated first by byte length, followed by a byte-by-byte comparison of their underlying memory buffers.
4. Collection Handling: Sets and Maps
For modern ES6 collections like Map and
Set, Lodash first checks the size property as
a quick-fail condition. If sizes match:
- Sets: Each item in the first set must have an equivalent deep match in the second set, regardless of insertion order.
- Maps: Keys are checked for deep equivalence, followed by a recursive check of the corresponding values.
5. Object and Array Traversal
For plain objects and arrays, Lodash executes a structured recursive traversal:
- Length/Key-Count Verification: Arrays are compared
by
length, and objects are verified by the count of their own enumerable properties usingObject.keys(). If counts differ, the check fails. - Prototype Equality: Lodash checks whether both
objects share the same
constructorproperty to prevent treating instances of different classes as equal, even if they contain the identical properties. - Recursive Property Matching: Lodash iterates through the keys of the first object, ensuring every key exists on the second object and recursively calling the internal equality function on the values.
6. Circular Reference and Cycle Detection
To prevent infinite recursion when traversing self-referencing or
circularly-linked data structures, Lodash maintains internal tracking
stacks (historically parallel arrays, modernly implemented via tracking
structures like Map or Set). Before evaluating
nested properties, the algorithm checks if the current pair of objects
is already undergoing comparison in the active call stack. If the pair
has already been traversed, Lodash treats the reference as equal and
unwinds the recursion.