Velocidad de Escape

In the vast landscape of JavaScript, efficient data handling is crucial, and two key players stand out: Iterators and Generators. These features empower developers to navigate and manipulate data in unique ways, offering enhanced control and flexibility in the iterative process.

Iterators serve as trusty guides, enabling the sequential traversal of collections or structures. With the Iterator Protocol at their core, these objects provide a standardized approach to accessing elements one at a time. From arrays to custom data structures, iterators lay the groundwork for orderly exploration.

Generators, on the other hand, introduce a dynamic twist to the narrative. These special functions possess the ability to pause and resume execution, making them invaluable in scenarios demanding asynchronous operations or lazy evaluation. Generators craft a narrative of iterative storytelling, allowing developers to yield values at their own pace.

Iterators

An “iterator” in JavaScript is an object that helps you go through a sequence of items. It has a next() method, which, when called, gives you the next item in the sequence. The result includes two properties: value (the actual item) and done (a boolean indicating if there are more items).

Examples

// Iterate over an array
let names = ['Alice', 'Bob', 'Charlie']

// Create an iterator
let namesIterator = names[Symbol.iterator]()

// Print one name at a time
console.log(namesIterator.next().value) // Alice
console.log(namesIterator.next().value) // Bob
console.log(namesIterator.next().value) // Charlie
// Iterate over the properties of an object
const library = {
  books: [
    { title: 'El Gran Gatsby', author: 'F. Scott Fitzgerald', genre: 'Ficción' },
    { title: '1984', author: 'George Orwell', genre: 'Distopía' },
    { title: 'Cien años de soledad', author: 'Gabriel García Márquez', genre: 'Realismo mágico' }
  ],
  [Symbol.iterator]: function () {
    let index = 0

    return {
      next: () => {
        if (index < this.books.length) {
          return { value: this.books[index++], done: false }
        } else {
          return { done: true }
        }
      }
    }
  }
}

// Create an iterator for the user object
const bookIterator = library[Symbol.iterator]()

let currentBook = bookIterator.next()

while (!currentBook.done) {
  const book = currentBook.value

  console.log(`Title: ${book.title}, Author: ${book.author}, Genre: ${book.genre}`)

  currentBook = bookIterator.next()
}

Generators

A “generator” in JavaScript is a special type of function denoted by the function* syntax. It can be paused and resumed, allowing you to control the flow of execution. Inside a generator, you use the yield keyword to produce values during each pause.

Examples

Let’s say you need to generate a simple sequence of even numbers using a generator:

function* generateEvenNumbers() {
  let i = 0

  while (true) {
    yield i
    i += 2
  }
}

const generator = generateEvenNumbers()

console.log(generator.next().value) // 0
console.log(generator.next().value) // 2
console.log(generator.next().value) // 4

Let’s imagine you’re building a task management application and need to perform asynchronous operations, such as fetching data from multiple sources, before processing and displaying the information. Here’s a complex example using generators to efficiently handle these operations:

// Simulated asynchronous function to fetch data from an external source
function fetchDataFromSource(source) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`${source} data`)
    }, Math.random() * 1000)
  })
}

// Generator coordinating fetching data from multiple sources
function* processTasks() {
  try {
    // Fetch data from the first source
    const source1Data = yield fetchDataFromSource('Source 1')
    console.log('Data from Source 1:', source1Data)

    // Fetch data from the second source
    const source2Data = yield fetchDataFromSource('Source 2')
    console.log('Data from Source 2:', source2Data)

    // Perform some processing with the obtained data
    const processedData = `Processed Data: ${source1Data} - ${source2Data}`
    console.log(processedData)
  } catch (error) {
    console.error('Error processing tasks:', error)
  }
}

// Helper function to run the generator
function runGenerator(generator) {
  const iterator = generator()

  function handleResult(result) {
    if (result.done) {
      return result.value
    }

    return result.value
        .then((data) => handleResult(iterator.next(data)))
        .catch((error) => iterator.throw(error))
  }

  return handleResult(iterator.next())
}

// Run the generator coordinating data fetching
runGenerator(processTasks)
  .then(() => console.log('Task processing complete'))
  .catch((error) => console.error('Error:', error))

In this example, processTasks is a generator that coordinates fetching data from two simulated asynchronous sources (Source 1 and Source 2). The runGenerator function manages the asynchronous execution of the generator.

This scenario simulates an application that needs to coordinate fetching data from multiple sources asynchronously before performing any processing. Generators provide a clean and synchronous-like mechanism for handling such complex and asynchronous operations.

Iterators vs Generators

FeatureIteratorsGenerators
Synchronous/Asynchronous IterationSynchronous onlySynchronous and asynchronous
Flow ControlBasicGreater control with yield
Method to Retrieve Elementsnext()next()
Creation SyntaxManual (implement interface)function* and yield
Key Fundamental KeywordN/Ayield
Iteration BacktrackingNo (unidirectional)Yes (can pause and resume)
Efficient Sequence GenerationNoYes (useful for infinite sequences)

When to use Iterators and Generators

Reason to UseIteratorsGenerators
Standard IterationProvides a standard interface for iteration.Allows both standard and customized iteration.
Data AbstractionEnables abstraction of iteration logic for clean code.Offers greater flow control for customization.
Compatibility with LoopsEasily integrates with loops like for...of.Facilitates integration with asynchronous operations.
CustomizationAllows customization of iteration logic.Provides granular control and pause/resume execution.
Handling AsynchronyNot inherently efficient for asynchronous operations.Efficient for asynchronous operations with yield.
Synchronous CodeProvides a synchronous way of iterating.Facilitates writing code that appears synchronous with yield.
Flow ControlOffers basic control during iteration.Enables more granular control with pause and resume.
Efficient SequencesNot specifically designed for generating sequences.Efficient for generating sequences, even infinite ones.
Error HandlingError handling through exceptions.Facilitates error handling with try...catch blocks.

I tried to explain this as clearly as possible, but it definitely still seems like a topic that is too complex and difficult to apply. If anyone has a clearer explanation, I would appreciate it…

Andres Bedoya

About Andrés Bedoya

JavaScript software engineer, internet enthusiast and blogger from an early age. He strongly believes in the free culture.

Learn more about Andrés Bedoya