En JavaScript una función generadora es un tipo especial de función que puede pausar su ejecución y luego reanudarla en un momento posterior.
Las funciones generadoras son útiles cuando necesitamos trabajar con una gran cantidad de datos o con secuencias grandes, ya que solo calculan los valores a medida que los solicitamos (en lugar de calcular todos los valores de una vez).
Sintaxis básica de las funciones generadoras
Las funciones generadoras se definen utilizando la sintaxis
function*
(el asterísco indica que la función es una generadora).Por otro lado, dentro de la función vamos a usar la palabra clave
yield
(que pausa la ejecución y devolver un valor)
Funcionamiento de yield
- Cada vez que se encuentra con un
yield
, la función devuelve el resultado y se suspende en ese punto. - Cuando se llama a
next()
de nuevo, la función generadora continúa ejecutándose justo después delyield
que la suspendió. - Se ejecutará hasta que encuentre otro
yield
o termine la ejecución.
El resultado devuelto por yield
es un objeto con las propiedades value
(el valor generado) y done
(un booleano que indica si la secuencia ha terminado).
Ejemplo básico
Vamos a verlo con un ejemplo. En primer lugar, vamos definir una función generadora muy sencilla, que devuelva una secuencia 1, 2 y 3.
function* generadorEjemplo() {
yield 1; // Pausa aquí y devuelve 1
yield 2; // Pausa aquí y devuelve 2
yield 3; // Pausa aquí y devuelve 3
}
En este ejemplo,
- La función
miGeneradora
genera tres valores secuenciales: 1, 2 y 3. - Cada
yield
devolverá un valor - La ejecución quedará suspendida, hasta que se la vuelva a invocar.
Ahora vamos a ver como podemos utilizar nuestra función generadora. Para ello, simplemente tenemos que invocar a su método next()
para solicitarle el siguiente valor.
const generador = generadorEjemplo();
console.log(generador.next()); // { value: 1, done: false }
console.log(generador.next()); // { value: 2, done: false }
console.log(generador.next()); // { value: 3, done: false }
console.log(generador.next()); // { value: undefined, done: true }
En este ejemplo,
- La función
next()
se llama repetidamente para obtener el siguiente valor. - El resultado obtenido contiene
value
ydone
Funciones generadoras e iteradores
La relación entre funciones generadoras e iteradores es muy estrecha (de hecho, están diseñados para trabajar juntas).
Un iterador es un objeto que debe tener un método next()
, que devuelva un objeto con las propiedades value
y done
.
Como hemos visto, este es exactamente el comportamiento de las funciones generadores cuando usamos yield
.
Es decir, que cualquier función generadora es también un iterador. De hecho, podemos usarlas en un bucle for...of
function* colores() {
yield 'rojo';
yield 'verde';
yield 'azul';
}
for (const color of colores()) {
console.log(color); // 'rojo', 'verde', 'azul'
}
En este ejemplo,
- El bucle
for...of
se encarga de llamar automáticamente anext()
en el generador y manejar el valor de retorno de cada iteración. - Cuando la función generadora termina de producir valores, el bucle
for...of
termina automáticamente.
Si quieres saber más sobre iterables e iteradores, consulta la entrada
Ejemplos prácticos
Secuencia de Fibonnacci
Veamos un ejemplo práctico de un generador que produce la secuencia de Fibonacci:
function* generadorFibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const iterador = generadorFibonacci();
console.log(iterador.next().value); // 0
console.log(iterador.next().value); // 1
console.log(iterador.next().value); // 1
console.log(iterador.next().value); // 2
console.log(iterador.next().value); // 3
console.log(iterador.next().value); // 5
// Y así sucesivamente...
En este ejemplo, el generador generadorFibonacci
produce la secuencia de Fibonacci de forma eficiente y sin necesidad de almacenar todos los valores en la memoria.
Generadores infinitos
Un caso de uso interesante es generar secuencias infinitas (lógicamente, esto sería imposible con arrays normales debido a que nos quedaríamos sin memoria).
function* secuenciaInfinita() {
let i = 0;
while (true) {
yield i++;
}
}
const generador = secuenciaInfinita();
console.log(generador.next().value); // 0
console.log(generador.next().value); // 1
console.log(generador.next().value); // 2
En este ejemplo,
- La función generadora
secuenciaInfinita
genera números consecutivos de forma infinita, incrementandoi
cada vez que se llama anext()
. - Como esta es una secuencia infinita, no tiene un final, y el código continuará generando números hasta que se interrumpa explícitamente.
Generadores con parámetros
Al igual que las funciones normales, las funciones generadoras pueden recibir parámetros. Esto puede ser útil si deseas personalizar la secuencia de valores que generan.
function* contarHasta(max) {
for (let i = 0; i <= max; i++) {
yield i;
}
}
const contador = contarHasta(5);
console.log([...contador]); // [0, 1, 2, 3, 4, 5]
En este caso, la función generadora recibe un parámetro max
y genera valores desde 0 hasta el valor máximo proporcionado.
Generadores asincrónicos
Una de las características avanzadas de las funciones generadoras es que pueden ser usadas con promesas para manejar flujos asincrónicos.
Las funciones generadoras asincrónicas se definen con la palabra clave async
y se utilizan junto con await
para trabajar con operaciones asincrónicas.
async function* generadorAsincrono() {
const data = await fetchData();
yield data;
}
async function fetchData() {
return 'Datos cargados';
}
const gen = generadorAsincrono();
gen.next().then(result => console.log(result.value)); // 'Datos cargados'
Comunicación bidireccional con yield
Podemos enviar datos a una función generadora utilizando el método next()
. Esto permite una comunicación bidireccional entre la función generadora y el código que la utiliza.
function* generadorInteractivo() {
const nombre = yield "¿Cómo te llamas?";
yield `Hola, ${nombre}!`;
}
const gen = generadorInteractivo();
console.log(gen.next().value); // "¿Cómo te llamas?"
console.log(gen.next("Juan").value); // "Hola, Juan!"
Delegación de generadores
Podemos delegar parte del trabajo de una función generadora a otra usando yield*
.
function* generadorA() {
yield 1;
yield 2;
}
function* generadorB() {
yield* generadorA();
yield 3;
}
for (const valor of generadorB()) {
console.log(valor);
}
// Salida:
// 1
// 2
// 3