Generators and Iterators in JavaScript: From Basics to Advanced

Generators and Iterators in JavaScript: From Basics to Advanced

Generators and iterators can be powerful tools in JavaScript, particularly when dealing with large or infinite data sequences. They let you process data efficiently without overloading memory. This article will introduce you to generator functions, the yield keyword, and practical examples to illustrate their uses.


1. What Are Iterators?

In JavaScript, an iterator is an object that allows you to traverse over a collection, like an array, one item at a time. It has a next() method, which returns an object containing:

  • value: the next value in the sequence
  • done: a boolean indicating whether there are more items in the collection

Example:

const numbers = [1, 2, 3];

const iterator = numbers[Symbol.iterator]();

console.log(iterator.next()); // { value: 1, done: false } 
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }        

2. What Are Generator Functions?

A generator function allows us to pause and resume execution. It is declared using function* (an asterisk after the function keyword). Inside a generator, we use the yield keyword to “pause” execution and return a value.

Example:

function* simpleGenerator() {
  yield 1;

  yield 2;

  yield 3;
} 

const gen = simpleGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }        

Each call to gen.next() returns the next yield value, making it perfect for handling sequences.


3. How to Pause and Resume Execution with yield

When a generator encounters a yield statement, it “yields” control back to the calling code. This allows you to create sequences and handle data that doesn’t load all at once, pausing until the next item is requested.

Example:

function* countDown(start) {
  while (start > 0) {
    yield start--;
  }

  yield "Blast off!";
}

const countdown = countDown(5);
console.log(countdown.next().value); // 5
console.log(countdown.next().value); // 4
console.log(countdown.next().value); // 3
console.log(countdown.next().value); // 2
console.log(countdown.next().value); // 1
console.log(countdown.next().value); // "Blast off!"        

4. Practical Use Cases for Generators

a) Handling Large Data Sets

Imagine you’re fetching data in chunks, like reading a large file line by line. Generators let you process each chunk as it arrives, preventing memory overload.

function* fetchDataInChunks(dataArray, chunkSize) {
  for (let i = 0; i < dataArray.length; i += chunkSize) {
    yield dataArray.slice(i, i + chunkSize);
  }
} 

const data = Array.from({ length: 1000 }, (_, i) => i + 1);
const chunkedData = fetchDataInChunks(data, 100); 

for (let chunk of chunkedData) {
  console.log(chunk); // Each chunk contains 100 elements
}        

b) Infinite Sequence Generators

A generator is perfect for generating an infinite sequence without consuming excessive memory. For example, generating Fibonacci numbers indefinitely:


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

}

const fibGen = fibonacci();
console.log(fibGen.next().value); // 0
console.log(fibGen.next().value); // 1
console.log(fibGen.next().value); // 1
console.log(fibGen.next().value); // 2
console.log(fibGen.next().value); // 3        

c) Managing Async Workflows

Generators can manage asynchronous code in a synchronous manner. This approach was popular before async/await was introduced but is still useful in certain cases.

function* asyncFlow() {
  const user = yield fetchUser();

  const posts = yield fetchPosts(user.id);

  return posts;
}

 

// Simulating API calls
 function fetchUser() {
  return new Promise((resolve) =>
    setTimeout(() => resolve({ id: 1, name: "Rohan" }), 1000)
  );

}

 function fetchPosts(userId) {
  return new Promise((resolve) =>
    setTimeout(() => resolve([{ title: "Post 1" }, { title: "Post 2" }]), 1000)
  ); 

}

const generator = asyncFlow();

generator.next().value.then((user) =>
  generator.next(user).value.then((posts) => console.log(posts))
);        

Here, we use yield to pause and wait for promises to resolve, mimicking async workflows.

5. Advanced Use: Two-Way Communication with Generators

Generators also support two-way communication. You can pass a value back to the generator using next().

Example:

function* interactiveGenerator() {
  const name = yield "What is your name?";

  const age = yield Hello, ${name}! How old are you?;

  yield Wow, ${name}! ${age} years old is amazing!;
}

const convo = interactiveGenerator();

console.log(convo.next().value); // "What is your name?"
console.log(convo.next("Rohan").value); // "Hello, Rohan! How old are you?"
console.log(convo.next(25).value); // "Wow, Rohan! 25 years old is amazing!"
        

This kind of interactive generator can be useful in apps that require real-time user input, like chatbots.


Conclusion

Generators and iterators provide unique capabilities in JavaScript for managing sequences and asynchronous workflows. They’re useful when handling large or infinite data, processing data in chunks, or building interactive workflows. By using yield to pause and resume execution, generators allow for efficient memory usage and more control over code execution.

Generators, though advanced, add versatility to JavaScript’s toolkit. Try using them in your next project to experience the difference they make in performance and code readability.

To view or add a comment, sign in

More articles by Sonu Tiwari

Insights from the community

Others also viewed

Explore topics