JavaScript

Generadores (function*) e Iteradores: pausando la ejecución y máquinas de estado finito

Aprende cómo funcionan iterables, iteradores y generators en JavaScript, cómo yield pausa la ejecución y por qué este modelo puede leerse como una máquina de estados finitos.

Contenido de la guía
JavaScript · iteración · pausa controlada · modelo mental

Generadores e iteradores: pausar ejecución sin magia

Mucha gente aprende function* como una rareza sintáctica. Ese es el error. Un generator no es “una función extraña que devuelve cosas por partes”: es una forma precisa de detener, conservar estado y reanudar una secuencia bajo demanda.

En esta guía vamos a separar con claridad tres piezas que suelen mezclarse: iterable, iterador y generator. Y después daremos el salto importante: entender por qué este modelo puede leerse como una máquina de estados finitos, útil para diseñar flujos más claros y más controlables.

Iterable Iterador next() yield Estado interno Máquina de estados

Qué vas a entender en esta guía

La diferencia real entre iterable e iterador

Veremos por qué un iterable no es lo mismo que un iterador, qué papel juega Symbol.iterator y por qué next() pertenece a otra capa del modelo.

Qué añade realmente un generator

Entenderás que function* no “rompe” las funciones normales: introduce una ejecución pausable y reanudable, donde cada yield conserva contexto entre pasos.

Por qué esto puede leerse como estados y transiciones

Daremos el salto desde sintaxis a modelo mental: cada pausa puede verse como un estado observable y cada llamada a next() como una transición.

Confusión central: pausar un generator no significa paralelismo, concurrencia ni multihilo. Un generator no ejecuta varias cosas al mismo tiempo. Lo que hace es ceder control, conservar su estado interno y continuar más tarde exactamente desde el punto donde quedó.

“Un generator no es magia ni asincronía automática: es una función que convierte su ejecución en una secuencia controlada de estados reanudables.”

Idea guía para leer toda la explicación
Relación entre iterable, iterador y generator.
Figura 1. Mapa mental inicial: una colección puede exponer el protocolo iterable; de ahí nace un iterador con next(); un generator automatiza ese patrón y permite pausar y reanudar la ejecución conservando estado.

El protocolo base antes de function*

Antes de hablar de generators, conviene fijar una idea más básica: en JavaScript, la iteración ya existe como protocolo. Un iterable promete que puede entregar un iterador; un iterador es el objeto que realmente avanza paso a paso por una secuencia mediante next().

Este punto importa porque un generator no inventa la iteración. Lo que hace es automatizar un patrón que ya existe: producir un iterador que conserva estado interno y puede reanudarse. Pero antes de llegar a eso, toca entender la base cruda del modelo.

Iterable, iterador y resultado de next()

Pieza Qué expone Qué resuelve Qué no es
Iterable Symbol.iterator Prometer que se puede iniciar un recorrido No avanza por sí mismo
Iterador next() Entregar la secuencia paso a paso No es necesariamente una colección
Resultado de next() { value, done } Informar qué valor salió y si el recorrido terminó No es el iterador mismo

Cómo funciona el protocolo en tres movimientos

1

Se pide un iterador

El recorrido no empieza “solo”. Primero se invoca objeto[Symbol.iterator]() para obtener el objeto que sabe avanzar.

2

El iterador responde con next()

Cada llamada produce un resultado estructurado. No entrega solo un valor: también informa si la secuencia sigue viva o ya terminó.

3

El estado avanza internamente

Después de cada next(), el iterador cambia de estado. Esa idea de “estado interno que progresa” es la base que luego los generators vuelven mucho más expresiva.

Idea clave: un iterable promete recorrido; un iterador ejecuta el avance. El generator todavía no entra aquí como magia nueva: entra después como una forma más elegante de construir y pausar ese iterador conservando contexto.

Un iterador manual, sin function*

Antes de usar generators, conviene ver el mecanismo en crudo. Este ejemplo muestra un objeto iterable que, al pedirle su iterador, devuelve un objeto con next(). Ahí ya está el protocolo completo, aunque todavía no exista ninguna pausa automática de ejecución.

const rango = {
  desde: 1,
  hasta: 3,

  [Symbol.iterator]() {
    let actual = this.desde;
    const hasta = this.hasta;

    return {
      next() {
        if (actual <= hasta) {
          return { value: actual++, done: false };
        }

        return { value: undefined, done: true };
      },
    };
  },
};

const iterador = rango[Symbol.iterator]();

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

Lectura correcta del ejemplo: el objeto rango es iterable porque expone Symbol.iterator. El objeto devuelto por ese método es el iterador real, porque es el que responde a next() y conserva el estado interno del recorrido.

Lo que debe quedarte claro antes de seguir: el protocolo iterable/iterador ya existe sin function*. Por eso, cuando entremos a generators, la pregunta ya no será “qué es iterar”, sino qué gana JavaScript al convertir esa iteración en una ejecución pausada y reanudable.

Qué añade realmente un generator

Hasta aquí ya vimos que JavaScript puede iterar sin function*. Existe un protocolo base, existe next() y existe un estado que avanza paso a paso. Lo que añade un generator no es “otra forma de iterar por gusto”, sino una mejora mucho más interesante: convertir la ejecución de una función en una secuencia pausable y reanudable.

Esa diferencia cambia el modelo mental completo. Una función normal entra, ejecuta y termina. Un generator, en cambio, puede avanzar hasta un yield, detenerse sin perder su contexto interno y continuar más tarde exactamente desde ese mismo punto cuando alguien vuelve a llamarlo con next().

Idea central: un generator no ejecuta “por partes” porque sí. Lo que hace es transformar una función en una secuencia de estados reanudables, donde cada yield marca una pausa observable y cada next() dispara la continuación.

Cómo se mueve un generator por dentro

1

Declararlo no lo ejecuta

Cuando defines una función con function*, todavía no ocurre el recorrido. Solo estás describiendo una función capaz de producir un generator.

2

Invocarlo crea el objeto generator

Al llamar la función, no recibes directamente un valor final. Recibes un objeto que actúa como iterador y que sabe cómo reanudar la ejecución poco a poco.

3

El primer next() inicia la ejecución real

Hasta que no aparece la primera llamada a next(), el cuerpo del generator sigue sin correr. Esa llamada arranca la ejecución y la empuja hasta el primer yield.

4

yield pausa y conserva contexto

Cuando el generator llega a un yield, entrega un valor, se detiene y conserva sus variables internas exactamente como quedaron.

5

El siguiente next() reanuda desde la pausa

La ejecución no vuelve al inicio. Continúa justo después del último yield, con el mismo estado interno que tenía al detenerse.

Un generator mínimo, leído paso a paso

El ejemplo más útil aquí no es uno “ingenioso”, sino uno que deje visible el comportamiento. Fíjate en este generator: no queremos admirar la sintaxis, sino observar con precisión cuándo empieza, dónde se pausa, qué entrega en cada punto y cómo termina.

function* contadorPausado() {
  yield "primer paso";
  yield "segundo paso";
  yield "tercer paso";
}

const secuencia = contadorPausado();

console.log(secuencia.next()); // { value: "primer paso", done: false }
console.log(secuencia.next()); // { value: "segundo paso", done: false }
console.log(secuencia.next()); // { value: "tercer paso", done: false }
console.log(secuencia.next()); // { value: undefined, done: true }

Lectura correcta del ejemplo: la llamada contadorPausado() no devuelve “el primer yield”. Devuelve un objeto generator. Los valores recién aparecen cuando ese objeto recibe llamadas sucesivas a next().

Qué ocurre antes, durante y después de cada yield

El siguiente ejemplo es más valioso porque muestra que el generator no solo “entrega valores”. También recorre una línea de ejecución real. Hay código que ocurre antes del primer yield, código que ocurre entre pausas y un cierre final que no se comporta igual que un yield.

Archivo lectura temporal

generator-traza.js · línea de ejecución observable

Este snippet deja ver el recorrido interno del generator. No muestra solo qué valores salen, sino cuándo se ejecuta cada parte del cuerpo y qué papel distinto cumplen yield y return.

Trazas yield return
function* flujo() {
  console.log("inicio del generator");
  yield "A";

  console.log("reanudado después de A");
  yield "B";

  console.log("reanudado después de B");
  return "fin";
}

const g = flujo();

console.log(g.next());
// consola:
// inicio del generator
// { value: "A", done: false }

console.log(g.next());
// consola:
// reanudado después de A
// { value: "B", done: false }

console.log(g.next());
// consola:
// reanudado después de B
// { value: "fin", done: true }

console.log(g.next());
// { value: undefined, done: true }

yield no significa lo mismo que return

Elemento Qué hace Efecto sobre la ejecución Estado final
yield Entrega un valor intermedio Pausa el generator Sigue vivo y puede reanudarse
return Entrega el valor de cierre Termina el generator Lo deja en done: true
Línea temporal de un generator con pausas y reanudación.
Figura 3. Un generator avanza solo cuando recibe next(): ejecuta hasta un yield, se pausa conservando su estado interno y continúa después desde ese mismo punto hasta cerrarse con return.

Lo que debe quedarte claro antes de seguir: un generator no es un callback, no es una promesa y no es una forma de concurrencia. Es una ejecución controlada: alguien la empuja con next(), el cuerpo avanza hasta un yield, conserva estado y queda listo para la siguiente transición.

De pausa de ejecución a máquina de estados

Hasta aquí ya vimos que un generator puede detenerse en un yield, conservar su contexto interno y continuar más tarde cuando recibe un next(). Ahora viene el salto que vuelve realmente valioso este modelo: leer esa ejecución pausada como una máquina de estados finitos.

La intuición es más simple de lo que parece. Si cada pausa deja visible un punto distinto del recorrido, entonces cada yield puede leerse como un estado observable. Y si cada llamada a next() empuja la ejecución al siguiente punto, entonces next() puede leerse como una transición. Ahí es donde el generator deja de ser “una función curiosa” y empieza a parecer una herramienta de modelado.

Idea central: cuando lees un generator como máquina de estados, yield deja de ser solo “un valor intermedio” y pasa a representar un estado en el que la ejecución queda detenida. A su vez, next() deja de ser solo “otra llamada” y pasa a representar la transición que reactiva el sistema.

Por qué esta lectura cambia el valor del patrón

Si te quedas solo con la sintaxis, un generator parece una forma elegante de ir entregando valores. Pero cuando lo lees como estados y transiciones, aparece algo mucho más útil: una forma compacta de modelar procesos que avanzan por etapas, conservan memoria y no deberían ejecutarse “de una sola vez”.

Eso lo vuelve interesante para flujos donde el sistema necesita recordar dónde va: asistentes paso a paso, parsers simples, procesos de validación, recorridos secuenciales y pequeños protocolos internos. En todos esos casos, el generator no reemplaza automáticamente una máquina de estados formal, pero sí ofrece una manera muy expresiva de representar una secuencia controlada con contexto persistente.

Dónde esta idea se vuelve útil de verdad

Wizard por pasos

Un formulario largo no necesita revelar todo al mismo tiempo. Puede avanzar paso a paso, conservar contexto y exponer un estado claro en cada transición.

Parser pequeño

Si un flujo debe leer tokens o etapas una por una, un generator permite conservar el punto actual sin reconstruir el recorrido completo en cada paso.

Proceso secuencial

Un semáforo, una revisión por etapas o una validación progresiva pueden leerse como estados que cambian cuando alguien dispara la siguiente transición.

Para que esta idea no quede en abstracto, conviene usar un ejemplo donde los estados sean visualmente claros. El semáforo funciona muy bien porque nadie confunde sus etapas: rojo, verde, amarillo y vuelta a empezar. Ahí se ve con nitidez qué parte del modelo corresponde al estado observable y qué parte corresponde a la transición.

Cómo mapear un generator a una máquina de estados

Elemento del generator Lectura como FSM Qué representa
Cuerpo interno Modelo del sistema La lógica que conserva memoria y define el recorrido posible
yield Estado observable El punto donde la ejecución se detiene y deja visible una etapa concreta
next() Transición El disparador que reactiva el sistema y lo empuja al siguiente estado
Variables internas Memoria del estado El contexto que sobrevive entre pausas sin reiniciarse
return Estado terminal El cierre definitivo del recorrido

Cómo leer el ejemplo del semáforo

1

Cada yield expone un color

El valor emitido no es “un dato cualquiera”: representa el estado actual del sistema en un punto preciso del recorrido.

2

Cada next() dispara el cambio

El semáforo no avanza solo. Necesita una transición explícita para pasar del estado actual al siguiente.

3

El contexto no se pierde entre estados

La ejecución no vuelve al inicio. El generator conserva su posición interna y sigue desde ahí, igual que una máquina de estados conserva su etapa actual.

function* semaforo() {
  while (true) {
    yield "rojo";
    yield "verde";
    yield "amarillo";
  }
}

const estado = semaforo();

console.log(estado.next()); // { value: "rojo", done: false }
console.log(estado.next()); // { value: "verde", done: false }
console.log(estado.next()); // { value: "amarillo", done: false }
console.log(estado.next()); // { value: "rojo", done: false }

Lectura correcta del código: aquí el generator no está simulando “una lista infinita” solo por diversión. Está modelando un sistema con estados cíclicos. Cada llamada a next() representa una transición, y cada valor producido por yield representa el estado en el que el sistema queda detenido.

Esto no sirve solo para ejemplos de colores

El semáforo ayuda a fijar el modelo, pero la idea útil aparece cuando cambias “rojo, verde, amarillo” por etapas de negocio o de interfaz. Por ejemplo: inicio, credenciales, verificación, acceso concedido. La estructura mental es la misma: estados observables, transición explícita y memoria interna conservada.

Generator leído como estados y transiciones

Si quieres quedarte con una sola imagen mental de esta sección, que sea esta: el generator no es solo una función que se pausa; es un recorrido donde cada pausa deja visible un estado y cada next() empuja el sistema hacia la siguiente transición.

Generator como máquina de estados con transiciones por next()
Figura 4. Un generator puede leerse como una máquina de estados: cada yield expone un estado observable y cada next() dispara la transición hacia el siguiente punto de ejecución.

Lo importante aquí no es forzar generators en cualquier problema. Lo importante es entender la ganancia conceptual: cuando un flujo necesita avanzar por etapas, conservar memoria y exponer puntos de pausa observables, un generator puede leerse como una máquina de estados pequeña, clara y muy expresiva.

Estás viendo solo el 60% del contenido. Hazte Premium para acceder a la guía completa.

Comunidad

Comentarios y valoraciones

No hay comentarios aún. ¡Sé el primero en opinar!