Javascript. Todo lo que necesitas saber sobre generadores
Una aproximación a las funciones “generadoras” de Javascript
Las funciones son un elemento clave en cualquier lenguaje de programación. Se emplean para realizar tareas específicas y pueden ser llamadas una y otra vez siempre que queramos.
La diferencia entre las funciones de Javascript y otros lenguajes de programación es que en Javascript las funciones son tratadas como “first-class objects”, es decir, se pueden asignar a variables, insertar en arrays y objetos y realizar cualquier otra operación que haríamos con los tipos base.
Cuando invocamos una función esperamos obtener más tarde o más temprano el resultado de la misma ya que lo habitual es que el código que contienen se ejecute de principio a fin sin que podamos detenerlo para reanudar su ejecución más tarde.
Sin embargo, una función generadora (“Generator Function”) sí puede ser detenida en medio de la ejecución de modo que cuando su callback es llamado retoma la ejecución desde el punto en que la dejó.
Una analogía para ilustrar qué son sería imaginarnos viendo una serie en Netflix hasta el momento en que suena el timbre con la pizza. En ese instante detenemos la serie que estamos viendo, recogemos la pizza y reanudamos la serie justo en el punto en que la dejamos.
Así que dicho esto, ¡Vamos a ver en qué consisten!
Funciones generadoras
A diferencia de las funciones normales, una función generadora puede ser detenida en medio de su ejecución y posteriormente retomarla desde el punto en que se detuvo mediante la ejecución del callback que proporcionan. Es decir:
- Nos van a permitir simplificar codificar iteradores (pues las funciones generadoras permiten devolver múltiples resultados).
- Podemos obtener secuencias de resultados en vez de un único resultado.
Las características de este tipo de funciones son las siguientes:
- Son declaradas mediante un
*
después de la palabra clavefunction
para diferenciarlas de las funciones normales. - Devuelven un objeto sobre el que podemos invocar el método
next()
.
Cada vez que invocamos next
sobre el objeto devuelto por la función generadora obtenemos un nuevo objeto con la siguiente estructura:
{
value: Any,
done: true|false
}
La propiedad value
es el valor devuelto por la función en ese paso mientras que done
indica si la función ha dado por concluida su ejecución o por el contrario tiene más elementos que devolver. En el momento en que se devuelve false
Javascript considera que la ejecución ha terminado.
Un ejemplo sencillo
Dado que el concepto de función generadora puede ser algo confuso al principio vamos a verlo con un ejemplo sencillo.

En este ejemplo declaramos la función generadora generatorFunction
cuya primera diferencia con una función normal es que en vez de emplear return
para devolver el valor final usa la palabra clave yield
.
Yield
es la forma de devolver valores dentro de una función generadora de modo que cuando un valor es devuelto de este modo la ejecución de la función generadora se detiene hasta que next
es vuelta a llamar.
En la línea 13 realizamos la primera ejecución de la función gen.next()
. en ese momento obtenemos el objeto:
{
done: false,
value: “first value”
}
En la línea 17 volvemos a invocar la función por medio de gen.next()
obteniendo ahora el objeto:
{
done: false,
value: “end of the function”
}
Finalmente, cuando volvemos a invocar gen.next()
en la línea 21, Javascript no encuentra más declaraciones yield
de modo que devuelve el objeto:
{
done: true,
value: undefined
}
Es importante destacar que si en algún momento empleamos return
en vez de yield
dentro de una función generadora, el objeto devuelto contendrá la propiedad done
establecida a true
y nada más será ejecutado.
function * generatorFunc() {
yield 'a';
return 'b'; // Generator ends here.
yield 'a'; // Will never be executed.
}
Implementando “iterables” mediante generadores
Una de las aplicaciones más comunes de las funciones generadoras es la implementación de iterables.
Cuando implementamos el patrón Iterable
en Javascript es necesario definir manualmente el método next
dentro del objeto. Además, es necesario guardar el estado para poder iterar correctamente a lo largo de la secuencia. Todo esto queda resuelto de forma prácticamente instantánea gracias a los generadores.
Por ejemplo, supongamos que tenemos definido el siguiente iterador para pintar por pantalla el texto Welcome to generators
:

Nota. Si te resulta curioso el uso de Symbol.iterator
aquí puedes leer su especificación.
Y a continuación la misma implementación con un generador:

Las diferencias son evidentes, ¿no?
- No es necesario recurrir a
Symbol.iterator
. - No es necesario implementar el método
next()
. - No es necesario devolver explícitamente el objeto
{value, done}
. - No es necesario almacenar el estado del mismo modo que hacemos dentro del iterador.
Generadores y Promises
Otra de las aplicaciones de las funciones generadoras la podemos encontrar a la hora de trabajar con código asíncrono.
Por ejemplo, cuando trabajamos con Promises es habitual tener código similar al siguiente:

Sin embargo, gracias a los generadores (y un poco de ayuda de librerías como co.js) podemos simplificarlo del siguiente modo:

Sí, por si lo habéis notado este código se parece mucho a la sintaxis async/await
que nos proporcionan las versiones modernas de Javascript y gracias a los cuales podemos reemplazar las Promises con funciones generadoras.
Flujos de datos infinitos
También es posible crear generadores que nunca terminen, por ejemplo:

Puesto que en cada iteración del bucle emplearemos yield
para devolver un resultado, siempre podremos obtener el siguiente número natural en la secuencia llamando a la función generadora naturalNumberGenerator
.
Y por si fuera poco…
Los generadores también pueden recibir valores empleando la sintaxis next(val)
procedentes de la función que la ejecutaron.
Cuando esto sucede el generador suele denominarse “observer” pues continua su ejecución cada vez que recibe un nuevo valor procedente del “subject” que la invocó.
Por ejemplo, si tenemos el siguiente código:
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`); // (1)
console.log(`2. ${yield}`); // (2)
return 'result';
}const genObj = dataConsumer();
Cuando invoquemos por primera vez el generador:
genObj.next()
// Started
// { value: undefined, done: false }
obtendremos en la consola el texto Started
y la ejecución continuará hasta el punto (1) donde aparece el primer yield
y se detendrá. El valor devuelto por genObj.next()
es el objeto con la propiedad value
a undefined
porque yield
no devuelve nada.
Ahora, si volvemos a invocar el generador pero con un arguemento:
genObj.next('a')
lo que obtendremos será:
1. a
{ value: undefined, done: false }
y aquí está lo importante. ${yield}
dentro del primer console.log
es sustituido por el valor pasado a través de next
y obtenemos por pantalla 1. a
.
A continuación, la ejecución prosigue hasta el yield
del punto 2 y vuelve a detenerse para devolver:
{ value: undefined, done: false }
Si volvemos a invocar ahora el generador:
genObj.next('b')
obtendremos en consola lo siguiente (dado que el yield
del segundo console.log
ha sido sustituido por 'b'
):
2. b
y el valor devuelto por next
será:
{ value: 'result', done: true }
concluyendo la ejecución.
Interesante, ¿verdad?
Ventajas de los generadores
Entre las ventajas de emplear generadores se encuentran:
- Se evalúan de forma “lazy” lo cual permite tener funciones que generan flujos de datos infinitos como vimos en el generador de números naturales. Es decir, los valores son calculados bajo demanda hasta el momento de ser necesitado.
- Son eficientes en la gestión de memoria. Puesto que sólo necesitamos los valores cuando los necesitamos, no estamos llenando la memoria con múltiples datos como sucede con las funciones normales. Gracias a los generadores podemos posponer su cálculo hasta el momento de necesitarlos.
- Por supuesto podemos combinar generadores en estructuras llamadas Combinators. Por ejemplo:

Esta función take
podemos emplearla del siguiente modo:
take(3, [‘a’, ‘b’, ‘c’, ‘d’, ‘e’]); // a b ctake(7, generateNaturalNumber());// 1 2 3 4 5 6 7
Desventajas
Pero como todo, también tienen algún que otro contra:
- Son de un sólo uso, es decir, una vez que hemos agotado todos los valores que pueden devolver no es posible volver a iterar sobre ellos, lo que nos obliga a crear un nuevo objeto con dicho generador.
- No permiten acceso aleatorio a posiciones a diferencia de como podemos hacer con los arrays pues los valores son generados de uno en uno en orden.
Conclusión
Como veis, los generadores abren un mundo de posibilidades a la hora de escribir código Javascript y definir el flujo de trabajo a la hora de trabajar con iteradores y Promises por lo que os recomiendo que os familiaricéis con esta herramienta tan potente.
Si este tema de los generadores os ha resultado interesante aquí os dejo un conjunto de referencias donde podéis leer más sobre ellos y familiarizaros con conceptos como yield *
, return
y throw()
.
¿Quieres recibir más artículos como este?
Si te ha gustado este artículo te animo a que te suscribas a la newsletter que envío cada domingo con publicaciones similares a esta y más contenido recomendado: 👇👇👇