Lodash _.partition Collection Mutation Behavior
This article explores how Lodash's _.partition method
behaves when a predicate function conditionally mutates the underlying
collection during evaluation. It breaks down the internal iteration
mechanics of Lodash, the consequences of index shifting and cached
bounds, and why in-place mutations lead to non-deterministic
partitioning results.
Internal Iteration
Mechanics of _.partition
In Lodash, _.partition is built on top of internal
aggregation utilities (such as createAggregator). When
invoked on an array, it iterates sequentially from index 0
up to length - 1 using an internal loop structure similar
to arrayEach.
Crucially, the iteration bounds—specifically the array's
length—are generally evaluated at the start of the
iteration cycle, and an internal counter increments monotonically by
1 on each step. Each element at the current index is passed
to the predicate function, and depending on whether the return value is
truthy or falsy, the element is pushed into one of two result arrays:
[pass, fail].
What Happens When Predicates Mutate the Collection
Because JavaScript arrays are reference types and Lodash iterates directly over the source reference without cloning it beforehand, modifying the collection inside the predicate directly alters the execution context of subsequent iterations.
1. Deleting Elements (e.g.,
splice)
If the predicate removes an element using methods like
Array.prototype.splice():
- The elements to the right of the removed item shift left to fill the gap.
- Because the internal loop counter increments regardless, the element that shifts into the current index is skipped entirely.
- The loop may attempt to access indices that no longer contain valid
elements near the original boundary, resulting in
undefinedvalues being evaluated and partitioned.
const numbers = [1, 2, 3, 4, 5];
const [evens, odds] = _.partition(numbers, (num, index, arr) => {
if (num === 2) {
arr.splice(index, 1); // Mutating the source array
return true;
}
return num % 2 === 0;
});
// Result:
// - `3` shifted into index 1 and was skipped.
// - `evens` receives [2, 4]
// - `odds` receives [1, 5]
// - `numbers` is permanently modified to [1, 3, 4, 5]2. Adding Elements (e.g.,
push, unshift)
push: Appending elements to the end of the array expands the collection. However, because Lodash uses a pre-calculated length in its internal array iterator, newly appended items are typically ignored and will not be evaluated by the partition logic.unshift: Prepending elements shifts existing items to higher indices, causing the loop to re-evaluate elements it has already processed, leading to duplicate assignments across the result partitions.
3. In-Place Element Property Mutation
If the predicate mutates the properties of an individual element rather than changing the array's structure, the mutation persists. The partitioned arrays store shallow references to the original objects. Any mutation applied to an object during the predicate's execution will be reflected in both the source array and the output partitions.
Summary of Behavioral Risks
- Skipped Iterations: Removing elements causes subsequent adjacent elements to bypass evaluation.
- Ignored Appends: Adding elements dynamically generally does not extend the partition loop.
- Reference Contamination: Modifying object properties inside the predicate permanently alters the input collection and the partitioned output.
To ensure predictable results, predicates passed to
_.partition should remain pure functions without side
effects. If the source collection must be modified, clone the collection
prior to partitioning or perform mutations in a separate processing
step.