Lodash toArray with Custom ES6 Class Iterables
This article examines how the Lodash utility function
_.toArray detects and consumes the ECMAScript 2015 (ES6)
iterable protocol on instances of custom classes. It breaks down the
internal sequence Lodash uses to inspect the
Symbol.iterator method, consume iterator streams via
low-level helper functions, and resolve conflicts between custom
iterables and array-like objects.
The Entry Point:
_.toArray
When you pass an argument to _.toArray(value), Lodash
first evaluates the falsiness of the input. If the value is
null, undefined, or otherwise falsey, it
immediately returns an empty array [].
For non-falsey inputs, Lodash evaluates the type and structural properties of the target to decide between three primary conversion strategies:
- Array-like objects: Handled via index copying.
- ES6 Iterables: Handled via iterator extraction and consumption.
- Plain objects: Handled by extracting enumerable
values via
values(value).
Detecting the Iterable Protocol
Custom ES6 classes qualify as iterables by implementing the standard
iteration protocol via the well-known symbol
Symbol.iterator. A typical class definition looks like
this:
class NumberSequence {
constructor(limit) {
this.limit = limit;
}
*[Symbol.iterator]() {
for (let i = 1; i <= this.limit; i++) {
yield i;
}
}
}When an instance of this class is passed to _.toArray,
Lodash checks if the object conforms to the iterable protocol.
Internally, Lodash checks if Symbol.iterator is defined on
the instance or its prototype chain:
const iterator = typeof value[Symbol.iterator] === 'function'
? value[Symbol.iterator]()
: undefined;If the function exists, Lodash invokes it with no arguments to obtain
the iterator object, which must implement the standard
.next() method.
The Consumption Phase:
iteratorToArray
Once Lodash obtains the iterator, it delegates the conversion to an
internal helper function called iteratorToArray.
The iteratorToArray function executes a standard
while loop over the iterator:
function iteratorToArray(iterator) {
let data;
const result = [];
while (!(data = iterator.next()).done) {
result.push(data.value);
}
return result;
}During this execution:
iterator.next()is called repeatedly.- Each emitted iteration result object is checked for the boolean
doneproperty. - If
doneisfalse(or falsey),data.valueis appended to the internal array. - When
done: trueis encountered, iteration stops, and the populated array is returned.
The isArrayLike
Conflict
A critical nuance in Lodash's resolution order is how it handles the
isArrayLike check.
Before inspecting for Symbol.iterator,
_.toArray checks whether the target satisfies
isArrayLike(value):
function isArrayLike(value) {
return value != null && typeof value !== 'function' && isLength(value.length);
}If an ES6 custom class explicitly defines a valid length
property (a non-negative safe integer), Lodash treats the instance as an
array-like object rather than an iterable:
class CustomCollection {
constructor() {
this.length = 2;
this[0] = 'a';
this[1] = 'b';
}
*[Symbol.iterator]() {
yield 'x';
yield 'y';
}
}
_.toArray(new CustomCollection()); // Returns ['a', 'b'], bypassing the iteratorIn this scenario, Lodash uses its internal copyArray or
slice logic based on indexed properties, completely bypassing
Symbol.iterator.
Summary of the Resolution Flow
For an instance of a custom ES6 class:
- Existence Check: If the class instance is
nullorundefined, return[]. - Array-Like Test: If the instance has a numeric
lengthproperty within bounds, copy its indexed properties (0throughlength - 1). - Iterator Protocol Test: If not array-like, check
for
typeof value[Symbol.iterator] === 'function'. - Iterator Drain: Invoke the method to get the
iterator and drain it into a new array using
iteratorToArray. - Fallback: If no iterator exists, treat the instance
as a generic object and return an array of its own enumerable property
values via
baseValues.