The Quirky Array Constructor and a Use for Holey Arrays in ES6

Datetime:2016-08-22 21:34:01          Topic: ECMAScript           Share

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"]
  1. Using two commas in a row within an array literal.
var myArray = ['one',,'three']
inspect(myArray)
// ["one", undefined × 1, "three"]
  1. 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:

  1. Repeat

The fill method on Array 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"]
  1. 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. Here's a great summary .

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 .





About List