DEV Community

EmNudge
EmNudge

Posted on

Generating Arrays in JS

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}}]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
alokjoshi profile image
AlokJoshiOfAarmax

Very useful.