Generadores en JavaScript
Los generadores en JavaScript son un tipo especial de función que permite pausar y reanudar su ejecución, devolviendo valores en múltiples etapas. Esta capacidad de “pausa” los convierte en una herramienta valiosa para manejar secuencias y flujos de datos complejos. En este artículo exploraremos cómo funcionan los generadores, su sintaxis y el uso de las palabras clave function*
y yield
.
¿Qué es un Generador?
Un generador es una función que puede detener temporalmente su ejecución y luego reanudarla desde donde se detuvo. Cada vez que un generador se “pausa”, puede devolver un valor sin perder su contexto. Esto se logra mediante la palabra clave yield
, que suspende la función y espera la siguiente llamada del método next()
para continuar.
Sintaxis básica:
function* nombreDelGenerador() {
// Código del generador
}
Para definir un generador se utiliza function*
en lugar de function
, lo que indica a JavaScript que la función es un generador.
Uso de yield
y next()
en Generadores
La palabra clave yield
permite pausar la ejecución de un generador y devolver un valor parcial. Cada vez que se invoca next()
, el generador avanza hasta la siguiente instrucción yield
o hasta que termina.
Ejemplo Básico de Generador
function* saludo() {
yield "Hola";
yield "¿Cómo estás?";
return "¡Adiós!";
}
const generador = saludo();
console.log(generador.next());
console.log(generador.next());
console.log(generador.next());
{ value: "Hola", done: false }
{ value: "¿Cómo estás?", done: false }
{ value: "¡Adiós!", done: true }
Entendamos paso a paso lo que pasó en el anterior ejemplo:
- La primera llamada a
next()
devuelve"Hola"
y se detiene. - La segunda llamada devuelve
"¿Cómo estás?"
. - La tercera llamada devuelve
"¡Adiós!"
y marcadone: true
, indicando que el generador ha terminado.
Diferencias entre Funciones Normales y Generadores
Los generadores y las funciones normales comparten la capacidad de encapsular bloques de código reutilizables, pero su funcionamiento y propósito son distintos. Mientras que una función normal sigue un flujo lineal y se ejecuta de principio a fin de una sola vez, los generadores permiten un control incremental sobre la ejecución, ofreciendo pausas y reanudaciones. A continuación las principales diferencias entre estos dos tipos de funciones y cómo impactan en su uso.
Funciones Normales:
- Ejecutan todo su código de una vez, desde el inicio hasta el final.
- Devuelven un solo valor con
return
.
Generadores:
- Pueden pausar su ejecución en cualquier punto usando
yield
. - Devuelven múltiples valores a medida que se va llamando a
next()
. - Mantienen su estado entre ejecuciones, permitiendo recordar variables y contexto.
Ejemplo Comparativo
// Función normal
function saludoNormal() {
return "Hola";
}
// Generador
function* saludoGenerador() {
yield "Hola";
yield "¿Qué tal?";
}
console.log(saludoNormal()); // Output: "Hola"
const generador = saludoGenerador();
console.log(generador.next().value); // Output: "Hola"
console.log(generador.next().value); // Output: "¿Qué tal?"
En el ejemplo anterior saludoNormal
devuelve su valor de inmediato, mientras que saludoGenerador
se puede pausar y continuar.
Aplicaciones Prácticas de los Generadores
Los generadores pueden ser útiles en situaciones donde es necesario manejar secuencias de datos grandes, realizar operaciones de “pausa y reanudación” o implementar funciones avanzadas de iteración.
Generador de Secuencias de Números
Los generadores son ideales para secuencias infinitas o series numéricas, permitiendo obtener valores solo cuando son necesarios.
function* generadorDeNumeros() {
let numero = 1;
while (true) {
yield numero++;
}
}
const secuencia = generadorDeNumeros();
console.log(secuencia.next().value);
console.log(secuencia.next().value);
console.log(secuencia.next().value);
1
2
3
Este generador produce una secuencia infinita de números incrementales, útil para generar identificadores únicos o manejar series sin límite.
Ejecución Condicional y Controlada con yield
A diferencia de las funciones normales, los generadores permiten ejecutar código de manera condicional, dependiendo de las llamadas sucesivas a next()
.
Ejemplo de Ejecución Controlada
function* flujoDeTrabajo() {
console.log("Iniciando...");
yield "Paso 1 completo";
console.log("En el paso 2...");
yield "Paso 2 completo";
console.log("Finalizando...");
return "Trabajo terminado";
}
const proceso = flujoDeTrabajo();
console.log(proceso.next().value);
console.log(proceso.next().value);
console.log(proceso.next().value);
"Iniciando..."
"Paso 1 completo"
"En el paso 2..."
"Paso 2 completo"
"Finalizando..."
"Trabajo terminado"
En el ejemplo anterior el generador flujoDeTrabajo
permite ejecutar el proceso en pasos controlados, pausando en cada yield
y reanudando según sea necesario.
Delegación de Generadores con yield*
A veces un generador necesita delegar la iteración a otro generador o iterable. yield*
permite incluir los valores de otro generador o iterable en la secuencia, de manera que el generador principal actúe como intermediario. Esto es útil para dividir la lógica en generadores más pequeños y combinarlos en uno solo.
function* generadorPrincipal() {
yield* generadorSecundario();
// código adicional en el generador principal
}
Ejemplo de yield* con Generadores Anidados
function* generador1() {
yield "Primero";
yield "Segundo";
}
function* generador2() {
yield "Inicio Generador 2";
yield* generador1(); // Delegación al generador1
yield "Fin Generador 2";
}
const iterador = generador2();
console.log(iterador.next().value);
console.log(iterador.next().value);
console.log(iterador.next().value);
console.log(iterador.next().value);
"Inicio Generador 2"
"Primero"
"Segundo"
"Fin Generador 2"
En el anterior codigo vemos que generador2
incluye los valores de generador1
dentro de su secuencia mediante yield*
, permitiendo que ambos generadores actúen como una única fuente de valores.
Enviar Valores a un Generador con next(value)
Además de avanzar el generador a la siguiente instrucción yield
, el método next()
también puede recibir un argumento que se pasará como resultado de la última instrucción yield
. Esto permite enviar información al generador durante la iteración, proporcionando una comunicación bidireccional.
const iterador = generador();
iterador.next(valor); // Envia el valor al generador
Ejemplo de next(value)
para Pasar Valores
function* calculadora() {
const num1 = yield "Ingresa el primer número:";
const num2 = yield "Ingresa el segundo número:";
yield `Resultado: ${num1 + num2}`;
}
const iterador = calculadora();
console.log(iterador.next().value);
console.log(iterador.next(5).value);
console.log(iterador.next(3).value);
"Ingresa el primer número:"
"Ingresa el segundo número:"
"Resultado: 8"
En el anterior ejemplo next(5)
y next(3)
envían los valores 5
y 3
al generador calculadora
, permitiendo realizar una suma dentro del generador. Esto es útil para construir flujos en los que el generador depende de datos externos en cada paso.
Buenas Prácticas al Usar Generadores
- Usa Generadores para Secuencias de Gran Tamaño: Al utilizar generadores se pueden manejar grandes cantidades de datos sin tener que almacenarlos en memoria, lo que es ideal para flujos de datos continuos.
- Evita Usar Generadores en Procesos Simples: Si solo necesitas devolver un valor simple, una función normal es más adecuada y eficiente.
- Combina Generadores con
for...of
para Iteración Sencilla: Los generadores se pueden recorrer usandofor...of
, lo cual permite iterar sobre cada valoryield
sin necesidad de llamar anext()
. - Divide Funciones Complejas en Generadores Más Pequeños: Usa
yield*
para delegar operaciones a otros generadores y así mantener el código modular y más fácil de leer. - Aprovecha
next(value)
para Comunicación Bidireccional: Cuando necesites retroalimentación en tiempo real, usanext(value)
para enviar y recibir datos dentro del generador. - Evita el Uso Excesivo de Generadores Infinito: Los generadores infinitos, como el de Fibonacci, pueden consumir recursos si no se controlan; úsalos con precaución o con condiciones de límite.
Conclusión
Los generadores en JavaScript son una herramienta poderosa para controlar la ejecución de funciones de manera pausada y secuencial. Con la ayuda de yield
y next()
los generadores permiten recorrer secuencias de datos grandes, gestionar procesos paso a paso y optimizar el uso de memoria. Al comprender cómo funcionan y cómo aplicarlos, puedes aprovechar al máximo sus capacidades para mejorar la eficiencia y flexibilidad de tu código.