Understanding the Array Constructor in ECMAScript
What are the different ways you can create arrays in javascript, and how are they different?
[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:
- 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']
- a)
- Using the Array constructor with more than one argument
- d)
var newArray = new Array(1,2,3) // [1,2,3]
- d)
- Using the literal syntax
- e)
var newArray = [1,2,3] // [1,2,3]
- f)
var newArray = [,,] // [empty,empty,empty]
- e)
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:
- Let
numberOfArgs
be the number of arguments passed to this function call.- Assert:
numberOfArgs
= 1.- If
NewTarget
is undefined, letnewTarget
be the active function object, else letnewTarget
beNewTarget
.- Let proto be ?
GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%")
.- Let
array
be! ArrayCreate(0, proto)
.- If
Type(len)
is not Number, then
- a. Let
defineStatus
beCreateDataProperty(array, "0", len)
.- b. Assert:
defineStatus
is true.- c. Let
intLen
be 1.- Else,
- a. Let
intLen
beToUint32(len)
.- b. If
intLen ≠ len
, throw aRangeError
exception.- Perform !
**Set(array, "length", intLen, true)**
.- Return
array
.
Here's the flow for var newArr = new Array(3)
.
- numberOfArgs is 1
- Assertion passes
- Handle the
new
or non-new
calls[2] - Gets us the prototype of the Array using
newTarget
from step 3. - 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.
- Len is a number, so we skip 6a-6c
- Woo, we create an
intLen
that is 3 in this case. - We set the length property on the array object to be equal to
intLen
. - 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
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." ↩︎
↩︎👏👏👏
— Kevin Ennis (@kevincennis) June 16, 2018
The `NewTarget` piece you weren't sure about: it's just saying you don't have to use the `new` operator to construct an array. So with `new`, the value of `newTarget` is explicitly `Array` (see `new.target`). But without `new`, they'll do that part for you.