Skip to content

forEach loop quirky behavior with undefined values?

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.

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 and map 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.