Understanding the Array Constructor in ECMAScript

[EDIT]: I realized a while back that I never posted this, but I think it still holds some information that's semi-useful or at least interesting.


TL;DR: RTFM on Array.forEach and Array constructor

So I've encountered what I thought was weird behavior when messing around with making arrays in my free time. I'll be honest, I don't always read the ECMAScript docs before I code something. I just kinda wing something and try it out in a REPL or console. For some reason, I was trying to test out building an array of things without typing them out (I think this was for testing something more chaotically, but who knows, now).

I popped my console open, and ran the following:

var constructedArray = new Array(3);
constructedArray.forEach( ( item, index ) => {
  console.log('I am an item at index ' + index);
});

So if you're like me, you might have thought the console would show this:

$ I am an item at index 0
$ I am an item at index 1
$ I am an item at index 2

Yeah? Give it a shot in the console here, I'll wait.


Well it turns out, I was wrong, and it confused me pretty greatly, so I decided to look into it. So in case anyone is searching for "why wont Array.forEach loop through my stupid new Array(3)?", here you go:

1) How do we create new arrays?

The first bit of info that we need to look into is how new arrays are created. I highly recommend diving into the ECMAScript docs to learn about all things. It takes a lot of hopping back and forth, highlighting, and headbutting, but you'll be glad you did as you should come away a little more confused in some areas, but a lot more knowledgeable in at least one.

OK, so the question is: why is it that the array built with the new keyword has empty entries, where an array that's instantiated with the "literal" syntax (e.g. var undefinedArray = [undefined, undefined, undefined]) does not?

Let's take a look at a few of the ways that an array is created:

  1. Using the Array constructor with exactly 0 or one argument (with or without the new keyword[1])
    • a) var newArray = new Array() // []
    • b) var newArray = new Array(3) // [empty,empty,empty]
    • c) var newArray = new Array('3') // ['3']
  2. Using the Array constructor with more than one argument
    • d) var newArray = new Array(1,2,3) // [1,2,3]
  3. Using the literal syntax
    • e) var newArray = [1,2,3] // [1,2,3]
    • f) var newArray = [,,] // [empty,empty,empty]

So based on the above methods of creation, I'm most interested in the new array used by calling the constructor, case b. The method goes like this:

  1. Let numberOfArgs be the number of arguments passed to this function call.
  2. Assert: numberOfArgs = 1.
  3. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget.
  4. Let proto be ? GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%").
  5. Let array be ! ArrayCreate(0, proto).
  6. If Type(len) is not Number, then
    • a. Let defineStatus be CreateDataProperty(array, "0", len).
    • b. Assert: defineStatus is true.
    • c. Let intLen be 1.
  7. Else,
    • a. Let intLen be ToUint32(len).
    • b. If intLen ≠ len, throw a RangeError exception.
  8. Perform ! **Set(array, "length", intLen, true)**.
  9. Return array.

Here's the flow for var newArr = new Array(3).

  1. numberOfArgs is 1
  2. Assertion passes
  3. Handle the new or non-new calls[2]
  4. Gets us the prototype of the Array using newTarget from step 3.
  5. Creates the Array using ArrayCreate. In this case, it's called with 0, which means it will create an Array exotic object (someday I'll know what that is), it will put a bunch of properties on that object, it will define a length property, and it will return the Array it created. So now we have that array object that we know and love.
  6. Len is a number, so we skip 6a-6c
  7. Woo, we create an intLen that is 3 in this case.
  8. We set the length property on the array object to be equal to intLen.
  9. We return. Huzzah.

What's interesting here isn't what we did, but specifically what we didn't do. Check out the other constructor methods, specifically this one where we pass more than 1 argument. In step viii, the iterator calls CreateDataPropertyOrThrow. This puts the argument that we would pass into the array that's being created.

Now check out option c) above. In that way, we don't skip steps 6a-6c, and lo and behold, we use the CreateDataProperty to set that element of the array to the value passed. so

Array('5') // ['5']
Array( { key:'value' } ) // [ { key: 'value' } ]

Summary

So hopefully, it's now a lot more clear why forEach would skip over these empty items. They never get set at all. And according to the ECMAScript docs...

...callbackfn is called only for elements of the array which actually exist; it is not called for missing elements of the array.

The takeaway of this journey through ramblings about Array creation and whatnot should be that you can avoid using the constructor when creating an array. Generating the array using the constructor syntax doesn't add items unless you pass it a non-number argument or multiple arguments, which negates the point of generating this empty array. If you still need to create that giant array for some reason, go ahead and fill it with something, and then loop through it. It adds a step, but it works for that very specific scenario.

var longArrayForSomeReason = Array(5).fill('lol').forEach( element => console.log(element) );
// lol
// lol
// lol
// lol
// lol

Hope this has been interesting! If you have any feedback or questions, please hit me up on twitter, I have no idea what I'm doing, and love to learn by doing :).

Cheers,
CL


  1. From here: "When called as a constructor it creates and initializes a new Array exotic object. When Array is called as a function rather than as a constructor, it also creates and initializes a new Array object. Thus the function call Array(…) is equivalent to the object creation expression new Array(…) with the same arguments." ↩︎

  2. ↩︎