Was writing a script in JS to make some dummy data for testing my API and ran into an interesting quirk with the forEach
loop in JS.
const dictionary = {}; const undefinedArray = Array(3); // [undefined, undefined, undefined] undefinedArray.forEach((_, index) => { console.log('Logging at index: ', index) const someObject = { id: index }; if (!dictionary[someObject.id]) { dictionary[someObject.id] = someObject } }); console.log(dictionary);
After checking the output of this snippet, you’ll see that nothing inside the forEach
loop is logged and the dictionary is still an empty object. I was talking with my coworker about this behaviour and he said he ran into this particular issue before and offered this as a solution.
const dictionary = {}; const undefinedArray = [...Array(3)]; // [undefined, undefined, undefined] undefinedArray.forEach((_, index) => { console.log('Logging at index: ', index) const someObject = { id: index }; if (!dictionary[someObject.id]) { dictionary[someObject.id] = someObject } }); console.log(dictionary);
By wrapping the Array constructor in square brackets and utilizing the spread operator, now the array is looped through and the object is built correctly. This fascinated me, so I went to the documentation for the Array object and found this:
arrayLength
If the only argument passed to the Array constructor is an integer between 0 and 2^32 – 1 (inclusive), this returns a new JavaScript array with its length property set to that number (Note: this implies an array of arrayLength empty slots, not slots with actual undefined values). If the argument is any other number, a RangeError exception is thrown.
So apparently it is not assigning each value undefined, but only setting its length property to whatever is passed in the constructor. This is not apparent when you log Array(n)
to the console because it shows an array with n undefined values. I assume the toString
method for the Array object is based on its length property and uses a normal for
or for of
loop to construct the string.
It does begin to make a little bit more sense, however, when you explicitly set an index of the newly defined array. In the snippet below, the same array is initialized, but the zero index is explicitly assigned undefined
as a value. Since this is an “actual undefined value” in Mozilla’s words, the forEach loop exectues at index zero.
const dictionary = {}; const undefinedArray = Array(3); // [undefined, undefined, undefined] undefinedArray[0] = undefined undefinedArray.forEach((_, index) => { console.log('Logging at index: ', index) const someObject = { id: index }; if (!dictionary[someObject.id]) { dictionary[someObject.id] = someObject } }); console.log(dictionary);
Array.map()
behaves the same way. So I guess my main question would be, are there other ways to execute forEach
and map
without filling the array or by using the quirky hack I mentioned earlier?
To recap: these are the two work arounds I’ve found for this particular use case:
[...Array(n)]
OR Array(n).fill()
. Both of these mutations to the array will allow a forEach
loop to iterate over all values in the array.
Advertisement
Answer
So apparently it is not assigning each value undefined, but only setting its
length
property to whatever is passed in the constructor.
Correct. (Provided you pass only a single argument and it’s a number. If you pass a non-number, or pass more than one argument, they’re used as elements for the array. So Array("3")
results in ["3"]
; Array(3, 4)
results in [3, 4]
.)
This is not apparent when you log Array(n) to the console because it shows an array with n undefined values.
It depends on what console you use. The devtools in Chromium browsers show (3) [empty x 3]
for exactly that reason, to differentiate between empty array slots and ones containing the value undefined
.
So I guess my main question would be, are there other ways to execute
forEach
andmap
without filling the array or by using the quirky hack I mentioned earlier?
If you want forEach
and map
to visit elements of the array, they have to actually exist. Those methods (and several others) are defined such that they don’t call your callback for empty slots in sparse arrays. If by “quirky hack” you mean [...Array(3)]
, that’s also filling the array (and is fully-specified behavior: [...x]
uses the iterator x
provides, and the array iterator is defined that it yields undefined
for empty slots rather than skipping them as forEach
, map
, and similar do). Doing that (spreading the sparse array) is one way to create an array filled with undefined
(not empty) elements. Array.fill
is another. Here’s a third: Array.from({length: 3})
const a = Array.from({length: 3}); a.forEach(value => { console.log(`value = ${value}`); });
Which you use is up to you. Array.from
is very simple and direct. Similarly Array(3).fill()
. I probably wouldn’t use the spread version (just because I think it’s fairly unclear to people who don’t have a deep knowledge of how the array iterator works), but it’s a matter of style.