Implementing ranges in JavaScript

Ranges are natively supported by a few (popular) programming languages. They allow for iteration over a defined space, while not having a linear increase in their memory footprint (all ranges always store a similar amount of data).

Let's try adding a similar idiom to JavaScript!


One way to approach this challenge is to write a plugin for a transpiler (for example a babel plugin) that would allow the following syntax:

const range = (0..5)
for (let i of range){
  console.log(i)
  // 0, 1, 2, 3, 4
}

Instead, we will provide a similar functionality with vanilla JavaScript.

for (let i of range(0, 5)) {
  console.log(i)
  // 0, 1, 2, 3, 4
}
We does the range stop at 4 instead of 5? This is a design choice, and as most languages that have a built-in range as not inclusive of their last value, the range utility that we will build will follow a similar convention by default.

The above syntax also allows us to pass a third argument to the function to control the step in between each iteration:

for (let i of range(0, 10, 2)) {
  console.log(i)
  // 0, 2, 4, 6, 8
}

To start, let's create a class Range which will host the data needed for a range:

class Range {
  constructor(start, stop, step = 1) {
    this._start = Number(start);
    this._stop = Number(stop);
    this._step = Number(step);

    // Initialise a counter for iteration
    this.i = Number(start);
  }
  
  first() {
    return this._start;
  }
  
  last() {
    return this._stop;
  }
  
  step() {
    return this._step;
  }
}

We can now create a very basic (and not very useful) range:

const range = new Range(0, 10);

range.first(); // 0
range.last(); // 10
range.step(); // 1 (by default)

One of the main reasons we want ranges though is to iterate over them ... so let's implement iteration protocols in our Range class!

To do so, we need to implement a next() method, as well as a [Symbol.iterator] method.

class Range {
  constructor(start, stop, step = 1) {
    ...

    // Initialise a counter for iteration
    this.i = Number(start);
  }
  
  first() { ... }
  last() { ... }
  step() { ... }
  
  next() {
    if (this.i < this._stop) {
      const value = this.i;
      this.i += this._step;
      return { value, done: false };
    }

    return { value: undefined, done: true };
  }
  
  [Symbol.iterator]() {
    return this;
  }
}

Great! Now we can use our ranges as follow:

const range = new Range(0, 5)

for(let i of range) {
  console.log(i)
  // 0, 1, 2, 3, 4
}

or

const range = new Range(0, 5)

range.next() // { value: 0, done: false }
range.next() // { value: 1, done: false }
range.next() // { value: 2, done: false }
range.next() // { value: 3, done: false }
range.next() // { value: 4, done: false }
range.next() // { value: undefined, done: true }

There is one issue with our current implementation though, and that is that the range is depleted after a single iteration. We cannot reuse the same range in multiple consecutive loops.

Luckily, there is a one line fix to support that:

class Range {
  constructor(start, stop, step = 1) {
    ...

    // Initialise a counter for iteration
    this.i = Number(start);
  }
  
  first() { ... }
  last() { ... }
  step() { ... }
  
  next() {
    if (this.i < this._stop) {
      const value = this.i;
      this.i += this._step;
      return { value, done: false };
    }
	
    // We reset the value once we have iterated over all values so that
    // ranges are reusable.
    this.i = this._start;

    return { value: undefined, done: true };
  }
  
  [Symbol.iterator]() {
    return this;
  }
}

Finally, to achieve the semantics we defined at the beginning, we need to wrap our class creation in a function:

class Range { ... }

function range(start, stop, step = 1) {
  return new Range(start, stop, step);
}

for (let i of range(0, 5)) {
  console.log(i)
  // 0, 1, 2, 3, 4
}

Again, inspired by this blog post, I decided to build a library with the aforementioned features and much more! Check it out:

https://github.com/AntonioVdlC/range

Antonio Villagra De La Cruz

Antonio Villagra De La Cruz

Multicultural software engineer passionate about building products that empower people.