It doesn't come up very often, but there is always a time in a young programmers life where they have to generate an array of numbers. Well... not always. Some programmers sometimes might maybe need to... well I did it once!
One such use case is in frontend frameworks where you need to display a set of numbers on a select
menu. I wouldn't personally recommend using a select
, but if it comes to a point where you're asked to by a client or boss, saying "no" doesn't fly so well.
Let's start with the simplest option and keep working up to more and more complex examples.
The For Loop
For all of our examples, let's try to generate the numbers 0-60 inclusive. Let's pretend we're using it for a user to choose a specific second or minute in a form. The for loop is probably the first example people think of when approached with this problem.
const arr = [];
for (let i = 0; i < 61; i++) {
arr.push(i);
}
We're simply incrementing i
and adding i
onto a predefined array each time we do increment. At the end of the day we get an array with 61 elements, 0-60 inclusive.
This approach is fine, but it's not "functional
" as it deals with a statement. This means we can't inline this in JSX
if we wanted to. We'd have to throw this into a function and call it in the render
. This isn't "bad" necessarily, just a bit extra.
The Array function
While we can pass comma-separated elements to Array()
, in order to create a new array, we can also supply just a single parameter. This would be a number which describes the length of the array to generate. This is a bit of a pitfall for us to keep in mind:
Array(50, 5) // -> [50, 5]
Array(50, 5).length // -> 2
Array(50) // -> [empty × 50]
Array(50).length // -> 50
What you might also notice is that we're creating an empty array with a length of 50
. We do not have 50 elements. This is the same as doing:
const arr = []
arr.length = 50;
These are called array "holes". We're used to undefined
taking place of undefined variables, but we're not actually changing anything except for the length of an empty array.
Now, we might think that we'd be able to generate an array with numbers 0-60 by just doing:
Array(61).map((_, i) => i) // -> [empty × 61]
but you'd be wrong. We are unable to iterate over empty
items.
Dr. Axel Rauschmayer talks about it more in depth here and here, but we're essentially going to need to fill our array with something in order to iterate over it.
We can do that one of 2 ways - using Array.prototype.fill
or Function.prototype.apply
.
Array(61).fill() // -> [undefined x 61]
Array.apply(null, Array(61)) // -> [undefined x 61]
I'd recommend the former (.fill()
) since it's a bit more readable and understandable. This turns our final expression into:
Array(61).fill().map((_, i) => i)
What if we wanted it to get a bit clearer?
Using Array.from
Array
has another method used a bit more with what is refereed to as "Array-like" data structures. Array.from
can be used to convert any object with a length
property into an array.
You might have seen Array.from
used in contexts like dealing with DOM nodes:
const divNodeList = document.querySelectorAll('div');
const divArr = Array.from(divNodeList);
const texts = divArr.map(el => el.textContent);
Array.from
will iterate over the numbered properties of the object until it hits the length property and replaces whatever it can't find with undefined
. We can actually recreate it fairly easily with JS:
const getArr = obj => {
const arr = [];
for (let i = 0; i < obj.length; i++) {
arr.push(obj[i]);
}
return arr;
}
This, funny enough, is actually a more optimized version of Array.from
. The bigger difference is that Array.from
allows a few more parameters and accepts an iterable, not just an array-like object. We'll get into iterables in the next section.
So how do we go about using Array.from
in our problem? If we pass Array.from
an object with only a length
property, we will get undefined in each position, unlike Array()
!
Array.from({}) // -> []
Array.from({ 2: 4, length: 4 }) // -> [undefined, undefined, 4, undefined]
Array.from({ length: 61 }) // -> [ undefined x 61 ]
Array.from({ length: 61 }).map((_, i) => i) // 0-60 inclusive
The cool thing here is that Array.from
accepts a second parameter - a map function! This means we can move our map inside the parentheses:
Array.from({ length: 61 }, (_, i) => i)
Iterators and Iterables
This should probably be its own post, but essentially we have what is referred to as "iterators". We loop over certain data structures without needing to access anything to do with an index. The data structure itself handles what the next value will be.
The topic is a bit much for this post, so I suggest checking out the MDN page for more information, but it's a really cool part of JS that allows the spread syntax and for...of loops to work.
Iterator functions get kinda complex when dealing with internal state, so we have Generator functions to help us create them.
function* makeIterator() {
yield 2;
yield 3;
yield 'bananas';
}
[...makeIterator()] // -> [2, 3, 'bananas']
We can think of each yield
as an element of the array in the order they appear. We use the spread syntax and surround it with brackets to turn it into an array. Also note how we require a *
to differentiate this from a normal function.
We can also use loops inside generator functions to yield many times
function* makeIterator() {
for (let i = 0; i < 4; i++) {
yield i;
}
}
[...makeIterator()] // -> [0, 1, 2, 3]
Data structures are iterable if they contain an @@iterator
property. This iterable is "well-formed" if the property follows the iterator protocol. We can give an object this property through Symbol.iterator
and we can follow the protocol by using a generator function.
We can also follow the protocol in other ways, but they're more than we're going to go through in this post.
Let's try to solve our problem using an iterable!
const iterable = {
[Symbol.iterator]: function*() {
yield 2;
yield 3;
yield 'bananas'
}
};
[...iterable] // -> [2, 3, 'bananas']
We have moved from a function to an iterable object. Now let's move the yields into a loop.
const iterable = {
[Symbol.iterator]: function*() {
for (let i = 0; i < 61; i++) {
yield i;
}
}
};
[...iterable] // 0-60 inclusive
Since we have an object, which is an expression, let's see if we can compress this down into 3 lines.
[...{*[Symbol.iterator]() {
for (let i = 0; i < 61; i++) yield i;
}}]
Nice! Not the prettiest, but it does what we want. Note that I've also changed Symbol.iterator]: function*()
into *[Symbol.iterator]()
as it's a bit shorter.
It should also be noted that all arrays are iterables. That's how they're able to be used with the spread syntax. The spread syntax also turns array holes into undefined
. That means we can change our Array()
example into:
[...Array(61)].map((_, i) => i)
which honestly looks a bit cleaner. We can even use an array buffer, a concept we're also not going to talk too much about, with the spread syntax for the same result!
[...new Uint8Array(61)].map((_, i) => i)
Preferences
Now we're down to which one to use.
We have a lot of options. When programmers have a lot of options we generally look at 2 things: style and performance.
With JS, it is generally said to not look at performance benchmarks as JIT compilers might optimize solutions to be faster one day where it wasn't faster the day before. Performance benchmarks, due to engine optimizations, are also many times extremely misleading.
With that in mind, the mutable array option seems to be consistently the fastest. Using Array()
with .fill()
or the spread syntax seems to come second, iterators third, and Array.from()
the last.
Array.from
can be recreated with a basic function for most use cases and be a better form of Array.from
if it's specialized for its specific use case, but unless you're calling it many times a second, I wouldn't sweat it.
The Array()
option with spread syntax seems to be the cleanest, but creating your own class for this very problem always seems a lot more fun:
class Range {
constructor(min, max, step = 1) {
this.val = min;
this.end = max;
this.step = step;
}
* [Symbol.iterator]() {
while (this.val <= this.end) {
yield this.val;
this.val += this.step;
}
}
}
Now you can use new Range(min, max[, step])
to generate an iterable of any range and just use the spread syntax to create arrays! A bit more verbose, but a bit more fun to use too!
What do you think? Any style preference?
Top comments (1)
Very useful.