Update 2019-01-02: Rewrote the section on performance. Switched from Array.from(new Array(LEN))
to Array.from({length: LEN})
(because the former argument can get large). New sections: “Recommended patterns”, “Acknowledgements”.
The best way of creating an Array, is via a literal:
const arr = [0,0,0];
Alas, that isn’t always an option, e.g. when creating large Arrays. This blog post examines what to do in those cases.
In most programming languages, an array is a contiguous sequence of values. In JavaScript, an Array is a dictionary that maps indices to elements. It can have holes – indices between zero and the length, that are not mapped to elements (“missing indices”). For example, the following Array has a hole at index 1:
> Object.keys(['a',, 'c'])
[ '0', '2' ]
Arrays without holes are also called dense or packed. Dense Arrays tend to perform better, because they can be stored contiguously (internally). Once there is at least one hole, the internal representation has to change. Two options are:
In either case, if an engine encounters a hole, it can’t just return undefined
, it must traverse the prototype chain and search for a property whose name is the index of the hole. That costs even more time.
In some engines, such as V8, the switch to a less performant data structure is permanent. They won’t switch back, even if all holes are plugged.
For more on how V8 represents Arrays, consult “Elements kinds in V8” by Mathias Bynens.
Array
constructor One common way of creating an Array with a given length, is to use the Array
constructor:
const LEN = 3;
const arr = new Array(LEN);
assert.equal(arr.length, LEN);
// arr only has holes in it
assert.deepEqual(Object.keys(arr), []);
This approach is convenient, but it has two downsides:
Array
constructor plus .fill()
method The .fill()
method changes an existing Array and fills it with a specified value. That helps with initializing an Array after creating it via new Array()
:
const LEN = 3;
const arr = new Array(LEN).fill(0);
assert.deepEqual(arr, [0, 0, 0]);
Caveat: If you .fill()
an Array with an object, all elements refer to the same instance (i.e., the object isn’t cloned):
const LEN = 3;
const obj = {};
const arr = new Array(LEN).fill(obj);
assert.deepEqual(arr, [{}, {}, {}]);
obj.prop = true;
assert.deepEqual(arr,
[ {prop:true}, {prop:true}, {prop:true} ]);
We’ll later encounter a way of filling (via Array.from()
) that doesn’t have this issue.
.push()
method const LEN = 3;
const arr = [];
for (let i=0; i < LEN; i++) {
arr.push(0);
}
assert.deepEqual(arr, [0, 0, 0]);
This time, we have created and filled an Array without putting holes in it. Therefore, using the Array after its creation should be faster than with the Array constructor. Alas, creating the Array is slower, because engines may have to reallocate the contiguous internal representation several times – as it grows.
undefined
Array.from()
converts iterables and Array-like values to Arrays. It treats holes as if they were undefined
elements. We can use that to convert each hole to an undefined
:
> Array.from({length: 3})
[ undefined, undefined, undefined ]
The parameter {length: 3}
is an Array-like object with length 3 that contains only holes. It is also possible to instead use new Array(3)
, but that usually creates larger objects.
Spreading into Arrays only works for iterable values and has a similar effect to Array.from()
:
> [...new Array(3)]
[ undefined, undefined, undefined ]
Alas, Array.from()
creates its result via new Array()
, so you still end up with a sparse Array.
Array.from()
You can use Array.from()
to map, if you provide a mapping function as its second parameter.
> Array.from({length: 3}, () => 0)
[ 0, 0, 0 ]
> Array.from({length: 3}, () => ({}))
[ {}, {}, {} ]
> Array.from({length: 3}, (x, i) => i)
[ 0, 1, 2 ]
> const START=2, END=5;
> Array.from({length: END-START}, (x, i) => i+START)
[ 2, 3, 4 ]
Another way of creating an Array with ascending integers is via .keys()
, which also treats holes as if they were undefined
elements:
> [...new Array(3).keys()]
[ 0, 1, 2 ]
.keys()
returns an iterable. We use spreading to convert it to an Array.
Filled with holes or undefined
:
new Array(3)
[ , , ,]
Array.from({length: 2})
[undefined, undefined]
[...new Array(2)]
[undefined, undefined]
Filled with arbitrary values:
const a=[]; for (let i=0; i<3; i++) a.push(0);
[0, 0, 0]
new Array(3).fill(0)
[0, 0, 0]
Array.from({length: 3}, () => ({}))
[{}, {}, {}]
(unique objects)Ranges of integers:
Array.from({length: 3}, (x, i) => i)
[0, 1, 2]
const START=2, END=5; Array.from({length: END-START}, (x, i) => i+START)
[2, 3, 4]
[...new Array(3).keys()]
[0, 1, 2]
I prefer the following approaches. My focus is on readability, not on performance.
new Array(LEN)
new Array(LEN).fill(0)
Array.from({length: LEN}, () => ({}))
Array.from({length: END-START}, (x, i) => i+START)
If you are dealing with Arrays of integers or floats, consider Typed Arrays – which were created for this purpose. They can’t have holes and are always initialized with zeros.
For most situations, I wouldn’t worry too much about performance. Even Arrays with holes are quite fast. It makes more sense to worry about your code being easy to understand.
Additionally, how and where engines optimize, changes. So what is fastest today, may not be tomorrow.