Harken back to a time when you needed to execute some code a specific number of times and had some reason to not use a for loop. You might’ve made a fresh array with a certain number of items, and then attempted to .forEach() over it:

const freshArray = new Array(5);

freshArray.forEach(item => {
	console.log(item);
});

If you did take that approach, you also might’ve expected to see five individual logs for undefined. But instead, nothing happened, as if the array didn’t have any items to begin with.

That’s because it technically doesn’t. When a number is passed into the Array constructor, no items are actually defined — not even undefined ones. Instead, only the length property is set, effectively reserving that many seats that may or may not be filled later. You’ve created a sparse array. You can verify that by logging the array itself:

const freshArray = new Array(5);

console.log(freshArray);

// [ <5 empty items> ]

When a method on the Array prototype (.map(), .forEach(), etc.) encounters such an array, it’s designed to skip over any uninitialized or deleted items. The position of those items doesn’t matter. For example, you could designate “empty” items by inserting commas:

const freshArray = [1, 2, , , 5];

console.log("Length:", freshArray.length);

freshArray.forEach(item => {
  console.log(item);
});

// Length: 5
// 1
// 2
// 5

And even by directly setting them via index:

const freshArray = [];

freshArray[3] = "fourth item!";

console.log("Length:", freshArray.length);

freshArray.forEach((item, index) => {
  console.log(item, index);
});

// Length: 4
// "fourth item!" 3

If you’re dead-set on using methods like .forEach() or .map() with a sparse array, you can reach for something like .fill() to populate each empty item with something first (even undefined).

Iterators Are Different, Though

That said, even those empty items are accessible using the Symbol.iterator method defined on the Array prototype. It’s implicitly accessed any time you use a for loop.

const freshArray = new Array(5);

for (const item of freshArray) {
  console.log(item);
}

// undefined
// undefined
// undefined
// undefined
// undefined

It’s also in play under the hood when the spread operator is used, which means you could spread your sparse array, and immediately be able to perform a .forEach() on it:

const freshArray = new Array(5);

// .forEach() will work now!
[...freshArray].forEach((item, index) => {
  console.log(item, index);
});

// undefined 0
// undefined 1
// undefined 2
// undefined 3
// undefined 4

If you wanted, you could use this iteration behavior to make your own variation of .forEach that’ll handle empty items (tack on methods to prototypes at your own risk!):

Array.prototype.inclusiveForEach = function(callback) {
  return [...this].forEach(callback);
}

const freshArray = new Array(5);

freshArray.inclusiveForEach((item, index) => {
  console.log(item, index);
});

// undefined 0
// undefined 1
// undefined 2
// undefined 3
// undefined 4

All of these decisions are up to you, and highly dependent on your circumstances. At the very least, I hope this helps shed some light on why the code behaves the way it does, and offers some considerations to help you move forward.