JS protip: Array.from() vs. Array#fill()
By Christophe Porteneuve • Published on 22 March 2023
• 3 min
Cette page est également disponible en français.
These days I keep stumbling onto people who seem to copy/paste this code snippet to produce an array of N copies of a unique value:
Array.from({ length: 10 }).fill(42)
// => [42, 42, 42, 42, 42, 42, 42, 42, 42, 42]
This happens to be needlessly verbose, but that doesn’t mean from()
isn’t super useful, far from it! So how do we pick the right tool for the job, then?
Always the same value: short and sweet with only fill()
JavaScript has always allowed initializing an array with a given length using Array(n)
. This is super concise, but no cell is actually defined, it’s kind of the ultimate sparse array: there are no cells!
This means operations like map()
and friends won’t do anything, or will return their default, “empty dataset” value:
Array(10)
// => [ , , , , , , , , , ] -- 10 indices, none defined
Array(10).map((n) => n * 2)[2]
// => undefined. No cell is used.
Array(10).every(() => false)
// => true (by default: no cell is looked at)
Array(10).some(() => true)
// => false (by default: again, no cell examined)
The established way of “filling in” such an array uses fill()
(duh), like so:
Array(10).fill(42)
// => [ 42, 42, 42, 42, 42, 42, 42, 42, 42, 42 ]
This is spiffy when the value is always the same (e.g. false
, 0
or the empty string ''
), which does happen frequently in the real world, usually at the beginning of some processing.
But what if we want multiple values?
Multiple values: let’s be smart about from()
At its core, Array.from()
is about turning an iterable into an actual Array
. It would usually consume a Map
, a Set
, codepoints from a String
or perhaps a DOM NodeList
, to list the most common scenarios.
Now, when the argument isn’t iterable, it needs to be at least “array-like,” which means it should have a non-negative integer length
property (and expectedly properties for indices from 0 to that), making it interoperable with most usual Array
methods.
Fun fact: Array.from({ length: 5 })
is not quite the same thing as Array(5)
: the latter doesn’t define any cell, whilst the former set all cells at… undefined
:
Array(5)
// => [ , , , , ]
2 in Array(5)
// => false (no [2] property is defined)
Array.from({ length: 5 })
// => [ undefined, undefined, undefined, undefined, undefined ]
2 in Array.from({ length: 5 })
// => true
This is cool and all, but the real banger is its second, optional argument: the mapper, the function that will transform items on-the-fly.
We could use that to generate any random value, or perhaps base them on the position, since that mapper receives not only the value but the index, much like array iteration methods (e.g. map()
and filter()
).
Array.from({ length: 10 }, (n, i) => i + 1)
// => [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
Array.from({ length: 5 }, () => Math.round(Math.random() * 100))
// => [ 65, 4, 62, 31, 73 ]
Let’s go all-out and implement a range()
utility, kind of like Lodash’s (the end
boundary is exclusive):
function range(start = 0, end, step = 1) {
if (end === undefined) {
;[start, end] = [0, start]
}
const length = Math.ceil((end - start) / step)
return Array.from({ length }, (u, index) => start + index * step)
}
range(5)
// => [ 0, 1, 2, 3, 4 ]
range(1, 5)
// => [ 1, 2, 3, 4 ]
range(1, 10, 2)
// => [ 1, 3, 5, 7, 9 ]
range(-1, -10, -2)
// => [ -1, -3, -5, -7, -9 ]
Sweeeet 😎
TL;DR
from()
can transform the source to generate values on-the-flyfill()
is more handy when using a unique value, and is shorter withArray(size)
- Both appeared in ES2015 (so they’re everywhere except IE, which is irrelevant now anyway)
References
The MDN’s interactive docs are as delightful as ever:
Protips galore!
We got tons of articles and videos, with a lot more to come. Also check out our kick-ass training courses 🔥!