DEV Community

Cover image for JavaScript's Iterators & Generators โš™๏ธ Refresher ๐Ÿš€
navvan
navvan

Posted on

JavaScript's Iterators & Generators โš™๏ธ Refresher ๐Ÿš€

Visit our official platform: Navvan

Follow us on: LinkedIn | YouTube

An In-Depth Look at JavaScript's Iteration Tools ๐Ÿ› ๏ธ

Iterators and generators are powerful features that provide a mechanism for customizing the behavior of forโ€ฆof loops. ๐Ÿ”„

Iterators: Unlocking the Power of Iteration ๐Ÿ”‘

Before diving into generators, let's first understand iterators. Most likely, you've already used iterators when working with arrays, maps, or even strings. (Yes, ๐Ÿ˜ฎ you've been using them!) ๐Ÿ“š

for (let char of 'hello') {
  console.log(char); // 'h', 'e', 'l', 'l', 'o'
}
Enter fullscreen mode Exit fullscreen mode

Any object that implements the iterable protocol can be iterated using a forโ€ฆof loop. ๐Ÿ”„ To make an object iterable, you need to implement the iterator method, which returns an iterator object. ๐Ÿงฑ

An iterator is an object that defines a sequence and potentially a return value upon termination. It has a next() โญ๏ธ method that returns an object with two properties:

  • value: The next value in the iteration sequence โฉ
  • done: true โœ… if the last value has been consumed, false โŒ otherwise

Here's how you can craft your range iterator: โœ๏ธ

function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;
  const rangeIterator = {
    next() {
      let result;
      if (nextIndex < end) {
        result = {
          value: nextIndex,
          done: false
        };
        nextIndex += step;
        iterationCount++;
        return result;
      }
      return {
        value: iterationCount,
        done: true
      };
    }
  };
  return rangeIterator;
}
Enter fullscreen mode Exit fullscreen mode

This iterator represents a sequence of numbers from start ๐Ÿ to end ๐Ÿ”š with a given step ๐Ÿชœ. Its next() โญ๏ธ method returns the next number in the sequence until the end is reached.

Creating Custom Iterators ๐Ÿ‘พ๐Ÿค–

While many built-in JS objects, such as arrays and strings, are iterable by default, you can also create custom iterators for your objects. To create a custom iterator, define a method with the key Symbol.iterator for your object, and this method should return an iterator object with a next() method.

For a range of numbers, you can construct a custom iterator like this:

class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        if (current <= end) {
          return {
            value: current++,
            done: false
          };
        } else {
          return {
            value: undefined,
            done: true
          };
        }
      },
    };
  }
}

const range = new Range(1, 3);
for (const number of range) {
  console.log(number); // 1, 2, 3
}
Enter fullscreen mode Exit fullscreen mode

While they may not be used every day, understanding how to leverage iterators and generators can be a great advantage when you encounter situations that require their unique capabilities. ๐Ÿ’ช

Generators: Simplifying Iterator Creationย โœจ

While custom iterators are useful, creating them requires careful ๐Ÿง state management. This is where generator functions come to the rescue! ๐Ÿฆธโ€โ™€๏ธ

Generator functions allow you to define an iterative algorithm by writing a single ๐Ÿ”‚ function whose execution is not continuous. They are written using the function* syntax and use the yield keyword to pause โธ๏ธ and resume ๐ŸŽฌ their execution.

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let iterationCount = 0;
  for (let i = start; i < end; i += step) {
    iterationCount++;
    yield i;
  }
  return iterationCount;
}
Enter fullscreen mode Exit fullscreen mode

When called, a generator function returns a special iterator called a Generator. Each time the generator's next() โญ๏ธ method is called, the function executes until it encounters the next yield expression. ๐Ÿงฎ

The yield keyword is similar to return, but instead of terminating the function, it pauses its execution until it encounters another next() โญ๏ธ call, behaving exactly like a breakpoint for iteration.

function* stringGenerator() {
  yield 'Iterators';
  yield 'Generators';
}

const strings = stringGenerator();
console.log(strings.next()); // { value: 'Iterators', done: false }
console.log(strings.next()); // { value: 'Generators', done: false }
console.log(strings.next()); // { value: undefined, done: true }
Enter fullscreen mode Exit fullscreen mode

Basic Generator Functions โš™๏ธ

To create a generator function, you need to use the function* syntax. Within this, you can use the yield keyword to pause the execution and return a value to the caller. When the generator is resumed by calling next(), it will continue executing from where it left off.

Here's a simple one that yields the numbers from 1 to 3:

function* numberGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = numberGenerator();
for (const number of generator) {
  console.log(number); // 1, 2, 3
}
Enter fullscreen mode Exit fullscreen mode

Nesting Generators

Since generators return iterators and those iterators are iterable, we can use yield* to nest generators, just like we nest functions.

function* getNumbers() {
  yield -3;
  yield -2;
  yield -1;
}

function* counterGenerator() {
  let i = 0;
  while (true) {
    yield i;
    i++;
  }
}

function* getNumbersThenCount() {
  yield* getNumbers();
  yield* counterGenerator();
}

for (let element of getNumbersThenCount()) {
  if (element > 4) {
    break;
  }
  console.log(element);
}
Enter fullscreen mode Exit fullscreen mode

Output:

-3 // <- getNumbers()
-2
-1
0 // <- counterGenerator()
1
2
3
4
Enter fullscreen mode Exit fullscreen mode

Use Cases for Generators and Iterators ๐ŸŽฏ

  1. Represent infinite sequences: Since generators compute values on-demand, they can efficiently represent sequences of unlimited size. โ™พ๏ธ
  2. Implement custom iteration behavior: Generators allow you to define custom iteration logic for your objects, giving you fine-grained control. ๐ŸŽ›๏ธ
  3. Simplify asynchronous code: Generator functions can be used to implement asynchronous iteration, allowing you to work with asynchronous data sources, such as APIs or streams, in a more streamlined way. ๐Ÿ“
  4. Implement lazy evaluation: Generators and iterators can be useful for implementing lazy evaluation, where values are only computed when they are needed. This can be particularly helpful for working with large data sets or expensive computations. ๐Ÿฆฅ

Bonus: ๐ŸŽˆ Endless Fibonacci Sequence with a Resetter

function* fibonacciGenerator() {
  let a = 0;
  let b = 1;
  while (true) {
    const reset = yield a;
    [a, b] = [b, a + b];
    if (reset) {
      a = 0;
      b = 1;
    }
  }
}

const generator = fibonacciGenerator();
// Get the first 10 Fibonacci numbers
for (let i = 0; i < 10; i++) {
  if (i === 8) {
    console.log(generator.next(true).value);
  }
  console.log(generator.next().value);
}
Enter fullscreen mode Exit fullscreen mode

Output:

0
1
1
2
3
5
8
13
0  // <- resetter ran here
1
1
Enter fullscreen mode Exit fullscreen mode

โ“ Can I use generators and iterators with other JavaScript data structures?

Yes, they can be used with a variety of JS data structures. Many built-in JS objects, like arrays, strings, and sets, already have default iterator implementations. We can also create custom iterators and generator functions for our objects and data structures.


Conclusion ๐ŸŽ‰

Generators and iterators are powerful tools in JavaScript that allow you to customize iteration behavior and create more expressive and readable code. Now that you know how they work under the hood, you can leverage their capabilities to solve complex problems elegantly.


If you enjoyed this article, please make sure to Like & Subscribe ๐ŸŒ

Visit our official platform: Navvan

Follow us on: LinkedIn | YouTube

Top comments (0)