Lodash Chaining: Map and Filter Evaluation Sequence
This article examines the exact evaluation sequence when chaining
multiple map and filter methods in the Lodash
JavaScript library. It explains the mechanics of Lodash's lazy
evaluation and shortcut fusion, contrasts this behavior with native
JavaScript execution, and details the step-by-step path each array
element takes through an operation pipeline before resolving the final
result with .value().
Eager vs. Lazy Evaluation: The Core Mechanism
In native JavaScript (Array.prototype.map and
Array.prototype.filter), chained operations execute
eagerly using horizontal processing:
- The first method processes every single element in the array and allocates a new intermediate array.
- The second method runs on the newly created array, again processing every element and allocating another intermediate array.
Lodash explicit chains (created via _(collection) or
lodash(collection)) operate differently using lazy
evaluation and shortcut fusion. Lodash does
not process operations horizontally. Instead, it fuses compatible
operations—specifically map and filter—into a
unified pipeline and processes data vertically, element
by element.
The Exact Lifecycle of Chained Execution
When you chain map and filter calls in
Lodash, execution proceeds through three distinct phases:
1. Deferred Pipeline Registration
Calling _(array).map(fn1).filter(fn2).map(fn3) does not
execute any iteratees immediately. Instead:
- Lodash wraps the array in a
LodashWrapperinstance. - Each method call appends an action descriptor (the iteratee function
and operation type) to an internal array called
__actions__. - Zero iteration over the source data occurs during this phase.
2. Triggering Execution via
.value()
Execution begins only when .value() (or its alias
.run()) is invoked on the chain wrapper. This triggers the
internal baseWrapperValue function.
3. Shortcut Fusion (Element-by-Element Processing)
Once triggered, Lodash reads from the original input array
sequentially. Rather than applying map to every item,
Lodash runs one single element through the entire chain of
iteratees before moving to the next element:
- Read Element
i: Lodash retrieves elementifrom the source collection. - First Operation (e.g.,
map): The element is transformed by the iteratee. The transformed value is temporarily held in memory. - Second Operation (e.g.,
filter): The transformed value is evaluated against the filter's predicate:- If the predicate returns
truthy: The value proceeds immediately to the next operation in the chain for this same element. - If the predicate returns
falsy: Lodash halts the pipeline for this element immediately. The remaining downstreammaporfilterfunctions are never called for this element. The engine discards it and proceeds directly to elementi + 1.
- If the predicate returns
- Subsequent Operations: Any further
maporfilteroperations run sequentially on that single element. - Collection: If the element passes all filter predicates, the final output value is appended to the final result array.
- Next Iteration: The process repeats for element
i + 1.
Concrete Execution Trace
Consider the following chain:
const result = _([1, 2, 3])
.map(n => {
console.log(`map1: ${n}`);
return n * 2;
})
.filter(n => {
console.log(`filter: ${n}`);
return n > 2;
})
.map(n => {
console.log(`map2: ${n}`);
return n + 10;
})
.value();The exact sequence of execution printed to the console is:
map1: 1— Element1is multiplied to2.filter: 2—2 > 2evaluates tofalse. Lodash drops the element immediately.map2is never called for1.map1: 2— Element2is multiplied to4.filter: 4—4 > 2evaluates totrue. Pipeline continues for this element.map2: 4— Element4is transformed to14and placed in the output array.map1: 3— Element3is multiplied to6.filter: 6—6 > 2evaluates totrue. Pipeline continues for this element.map2: 6— Element6is transformed to16and placed in the output array.
Exceptions to Shortcut Fusion
This fused, element-by-element sequence applies strictly to
operations that can be evaluated independently per element. If an
operation requiring global knowledge of the collection (such as
sortBy, reverse, or shuffle) is
inserted between map and filter, shortcut
fusion breaks at that boundary:
- Lodash flushes the fused pipeline up to the barrier method.
- The barrier method creates an intermediate array and sorts/reorders it.
- A new lazy pipeline begins for any subsequent
maporfiltercalls.