In JavaScript, an Array
isn’t like your typical array: it’s an object with keys that are indexes. What this means is that you need to be aware that the length can be more than the number of elements and that there can be “holes”.
The Array Constructor
In day-to-day development you’ll probably just build up arrays using the literal syntax, like so:
var myArray = ['An element', 'Another']
However, there is a constructor for Array
that initializes the length:
// Initialize an array with a length of 2, but without any elements.
var twoHoles = new Array(2)
twoHoles.length
// <- 2
You might think this creates an array with two empty spots for elements, but you’d be wrong! Instead, it literally just creates a blank array with the length
property set to 2
; it doesn’t even assign undefined
into the array at each index.
// No elements logged.
console.log(twoHoles)
// <- []
// Looping over elements here prints nothing.
twoHoles.forEach((element, index) =>
console.log('Element ' + index + ' is ', element)
)
// <- undefined
However, most operations treat these holes as undefined
:
// inspecting: it knows there's 2 holes...
inspect(twoHoles)
// <- [undefined √ó 2]
// Join those elements that aren't even there.
twoHoles.join(",")
// <- ","
This is true also for operations that change the array:
var aChangedArray = Array(2) // Two holes.
aChangedArray.push('third') // Append a "third".
aChangedArray[0] = 'first' // Fill the first hole.
console.log(aChangedArray)
// <- ["first", 2: "third"]
inspect(aChangedArray)
// <- ["first", undefined √ó 1, "third"]
However, map
will skip holes, in effect preserving them:
aChangedArray.map(() => "*")
// <- ["*", undefined x 1, "*"]
As you can see, it’s actually pretty inconsistent.
The Quirkiness
The Array
constructor behaves quite differently depending on the number of arguments and their types. If you pass more than one argument then it will interpret the arguments as elements for the new array:
var twoArguments = Array('A', 'B')
// <- ['A', 'B']
Actually, it still does even if you pass in one argument:
var oneArgument = Array('A')
// <- ['A']
The only way to make the constructor consider the argument as the length is by giving it an argument which is a number:
var eightHolesNotAnArrayWithOneNumber = Array(8)
// <- [ undefined x 8 ]
Which doesn’t work too well for floats:
var someNumbersFail = Array(2.3)
// <- Uncaught RangeError: Invalid array length()
Other Hole Punchers
Holes can creep in a few other interesting ways: 1. Deleting an index.
var myArray = ['one', 'two', 'three']
delete myArray[0]
inspect(myArray)
// [undefined √ó 1, "two", "three"]
- Using two commas in a row within an array literal.
var myArray = ['one',,'three']
inspect(myArray)
// ["one", undefined √ó 1, "three"]
- Increasing the
length
property.
var myArray = ['one', 'two', 'three']
myArray.length = 10
inspect(myArray)
// ["one", "two", "three", undefined √ó 7]
A Use for Holey Arrays?
So should we put this down to a weird historical oddity, and just avoid it? Well, generally, yes. However, there are a few cases where using the constructor to produce an array with a defined length works really well:
- Repeat
The
fill
method onArray
will repeat an element for each element in the array, and doesn’t care if it’s a hole or not:
Array(4).fill("Bob")
// ["Bob", "Bob", "Bob", "Bob"]
- Ranges
The
keys
method returns an iterator for indexes of each element, effectively producing a range of numbers.
for (let num of Array(4).keys()) {
console.log(num)
}
// 0
// 1
// 2
// 3
Note that an iterator isn’t an Array; it’s an object that you can call next
on. It works with for...of
and the spread
operator:
[...Array(4).keys()]
// <- [0, 1, 2, 3]
Array.from
Unfortunately, with new functions introduced in ES6, holes are treated even more inconsistently.
Maybe a better alternative is to avoid the Array
constructor, and use the new Array.from
function. It takes anything that looks like an array, and an optional map function, and produces a real array. An object “looks” like an array if it has a length and allows element lookup by using []
. An object literal with a length
property does this just fine:
// No holes here.
Array.from({length: 3})
// [undefined, undefined, undefined]
We can produce very similarly compact code for repeat
and range
:
// Repeat.
Array.from({length: 4}, () => 'Bob')
// ["Bob", "Bob", "Bob", "Bob"]
// Ranges.
Array.from({length: 10}, (_, i) => i)
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Conclusion
Holes are best avoided in JavaScript, since whether a method sees them, ignores them or just preserves them is very inconsistent. However, using them in conjunction with the Array
constructor allows for some expressive and compact constructs. A better approach may be to use Array.from
.