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 ⌄
- Generadores e iteradores: pausar ejecución sin magia
- Qué vas a entender en esta guía
- El protocolo base antes de function*
- Iterable, iterador y resultado de next()
- Cómo funciona el protocolo en tres movimientos
- Se pide un iterador
- El iterador responde con next()
- El estado avanza internamente
- Un iterador manual, sin function*
- Qué añade realmente un generator
- Cómo se mueve un generator por dentro
- Declararlo no lo ejecuta
- Invocarlo crea el objeto generator
- El primer next() inicia la ejecución real
- yield pausa y conserva contexto
- El siguiente next() reanuda desde la pausa
- Un generator mínimo, leído paso a paso
- Qué ocurre antes, durante y después de cada yield
- yield no significa lo mismo que return
- De pausa de ejecución a máquina de estados
- Por qué esta lectura cambia el valor del patrón
- Dónde esta idea se vuelve útil de verdad
- Cómo mapear un generator a una máquina de estados
- Cómo leer el ejemplo del semáforo
- Cada yield expone un color
- Cada next() dispara el cambio
- El contexto no se pierde entre estados
- Esto no sirve solo para ejemplos de colores
- Generator leído como estados y transiciones
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.
Qué vas a entender en esta guía
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.
Entenderás que function* no “rompe” las funciones normales: introduce una ejecución pausable y reanudable, donde cada yield conserva contexto entre pasos.
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.”
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
Se pide un iterador
El recorrido no empieza “solo”. Primero se invoca objeto[Symbol.iterator]() para obtener el objeto que sabe avanzar.
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ó.
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.
iterador-manual.js · protocolo base sin generators
Este snippet muestra la diferencia entre iterable e iterador sin introducir todavía yield. La colección expone Symbol.iterator; el iterador real expone next().
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
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.
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.
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.
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.
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.
generator-basico.js · pausa y reanudación mínima
Este ejemplo no busca impresionar. Su propósito es mostrar, con la menor ambigüedad posible, que function* devuelve un generator, que yield pausa la ejecución y que cada next() la reanuda desde el punto correcto.
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.
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.
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 |
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
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.
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.
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
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.
Cada next() dispara el cambio
El semáforo no avanza solo. Necesita una transición explícita para pasar del estado actual al siguiente.
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.
generator-semaforo.js · estados y transiciones visibles
Este ejemplo no usa yield para “soltar valores por partes”. Lo usa para modelar estados observables. El semáforo se detiene en cada color y solo cambia cuando una transición lo empuja con next().
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.
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.
Comentarios y valoraciones
No hay comentarios aún. ¡Sé el primero en opinar!