Many programming languages have some form of range() function that lets you create a range of numbers. For example, PHP has a range() function, Python has a range type, and Ruby has Range objects. JavaScript doesn't have any of these, but one solution is to create a range of numbers using the Array.from() method.

/**
* Create a range of numbers.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#sequence_generator_range
* @param {number} start The first number in the range.
* @param {number} stop The last number in the range.
* @param {number} step The step between each number in the range.
* @returns {number[]} A range of numbers.
*/

function range(start, stop, step) {
return Array.from(
{ length: (stop - start) / step + 1 },
(_, i) => start + i * step
);
}

I found this range() function in the MDN Web Docs. All I did was turn it into a function declaration (instead of a single-line arrow function expression) and document it using JSDoc. It's really clever, so I'd like to explain how it works!

Array-like objects

The Array.from() method creates a new array from an iterable or array-like object. Iteration protocols are outside the scope of this article, but an array-like object is an object with a .length property and indexed elements. For example, NodeList and HTMLCollection objects are array-like.

Why do these qualities make an object “array-like”? Because they're definitive qualities of arrays! Arrays are just a type of object and their indices are just properties. I talked about this when I explained why the Array.prototype.at() method exists. For a quick summary, consider the following code:

const colors = ["red", "green", "blue"];
Object.getOwnPropertyDescriptors(colors);

In this case, the return value of the Object.getOwnPropertyDescriptors() method is an object with the following structure:

{
"0": {
configurable: true,
enumerable: true,
value: "red",
writable: true
},
"1": {
configurable: true,
enumerable: true,
value: "green",
writable: true
},
"2": {
configurable: true,
enumerable: true,
value: "blue",
writable: true
},
length: {
configurable: false,
enumerable: false,
value: 3,
writable: true
}
}

This tells us that the value of the colors constant (an array) is actually an object with four of its own properties (properties not inherited from Array.prototype). The value of the ["0"] property is "red", the value of the ["1"] property is "green", and the value of the ["2"] property is "blue". These correspond to the indexed elements in the array. The value of the .length property is 3, which reflects the number of indexed elements in the array.

Calculating the length of the array

With all that said, we can talk about the first argument to the Array.from() method inside our range() function. We're passing in an array-like object! It has a .length property, although it doesn't have any indexed elements:

{ length: (stop - start) / step + 1 }

To calculate the number of elements in the new array, we're dividing the difference between the stop and start values by the step value. Then we +1 because arrays are zero indexed.

For example, if we wanted an array of numbers from 1 to 10, we would invoke the range() function with the arguments 1, 10, and 1. The value of the .length property would be calculated as (10 - 1) / 1 + 1 which is equal to 10. This means there would be 10 elements in the new array.

Astute readers might have noticed that the value of the .length property could be a floating-point number. For example, if we invoked the range() function with the arguments 5, 9, and 3, then the value of the .length property would be calculated as (9 - 5) / 3 + 1. This is approximately equal to 2.333333333333333. It works because the value of the .length property is normalized:

The length property is converted to a number, truncated to an integer, and then clamped to the range between 0 and 253 - 1. NaN becomes 0, so even when length is not present or is undefined, it behaves as if it has value 0.

Filling the array

Because there aren't any indexed elements in our object (it only has a .length property), it isn't truly array-like. The Array.from() method will account for this by using the value undefined for each of the elements in the new array.

This is where the second argument to the Array.from() method comes in. The Array.from() method accepts an optional map function which it will invoke on each element of the array being created.

More clearly, Array.from(obj, mapFn, thisArg) has the same result as Array.from(obj).map(mapFn, thisArg), except that it does not create an intermediate array, and mapFn only receives two arguments (element, index) without the whole array, because the array is still under construction.

We're using an anonymous arrow function expression as our map function:

(_, i) => start + i * step

The first parameter refers to the current value in the array. In our case, this will always be undefined. Because we're not using the value, we're using an underscore (_) to signify that it needs to be there and shouldn't be removed. This is just a convention.

The second parameter refers to the index (i) of the current value in the array. We're returning the starting value (start) plus the product of the index (i) and the step value. This makes sense if you think about it: we're starting with the start value and adding i number of steps. For example, if we invoked range(3, 9, 2), the return values would be:

  • 3 + 0 * 2 which is equal to 3
  • 3 + 1 * 2 which is equal to 5
  • 3 + 2 * 2 which is equal to 7
  • 3 + 3 * 2 which is equal to 9

Demo

Here are a few examples and a demo you can play with.

range(1, 10, 1); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
range(5, 25, 5); // [5, 10, 15, 20, 25]
range(0, 12, 3); // [0, 3, 6, 9, 12]
range(-1, -9, -1); // [-1, -2, -3, -4, -5, -6, -7, -8, -9]