Las funciones regulares devuelven solo un valor único (o nada).
Los generadores pueden producir (“yield”) múltiples valores, uno tras otro, a pedido. Funcionan muy bien con los iterables, permitiendo crear flujos de datos con facilidad.
Funciones Generadoras
Para crear un generador, necesitamos una construcción de sintaxis especial: function*
, la llamada “función generadora”.
Se parece a esto:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
Las funciones generadoras se comportan de manera diferente a las normales. Cuando se llama a dicha función, no ejecuta su código. En su lugar, devuelve un objeto especial, llamado “objeto generador”, para gestionar la ejecución.
Echa un vistazo aquí:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
// "función generadora" crea "objeto generador"
let generator = generateSequence();
alert(generator); // [object Generator]
La ejecución del código de la función aún no ha comenzado:
El método principal de un generador es next()
. Cuando se llama, se ejecuta hasta la declaración yield <value>
más cercana (se puede omitir value
, entonces será undefined
). Luego, la ejecución de la función se detiene y el value
obtenido se devuelve al código externo.
El resultado de next()
es siempre un objeto con dos propiedades:
value
: el valor de yield.done
:true
si el código de la función ha terminado, de lo contrariofalse
.
Por ejemplo, aquí creamos el generador y obtenemos su primer valor yield:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
let one = generator.next();
alert(JSON.stringify(one)); // {value: 1, done: false}
A partir de ahora, obtuvimos solo el primer valor y la ejecución de la función está en la segunda línea:
Llamemos a generator.next()
nuevamente. Reanuda la ejecución del código y devuelve el siguiente yield
:
let two = generator.next();
alert(JSON.stringify(two)); // {value: 2, done: false}
Y, si lo llamamos por tercera vez, la ejecución llega a la declaración return
que finaliza la función:
let three = generator.next();
alert(JSON.stringify(three)); // {value: 3, done: true}
Ahora el generador finalizó. observamos done: true
y procesamos value: 3
como el resultado final.
Las nuevas llamadas a generator.next()
ya no tienen sentido. Si las hacemos, devuelven el mismo objeto: {done: true}
.
function* f(…)
o function *f(…)
?Ambas sintaxis son correctas.
Pero generalmente se prefiere la primera sintaxis, ya que la estrella *
denota que es una función generadora, describe el tipo, no el nombre, por lo que debería seguir a la palabra clave function
.
Los Generadores son iterables
Como probablemente ya adivinó mirando el método next()
, los generadores son iterables.
Podemos recorrer sus valores usando for..of
:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, then 2
}
Parece mucho mejor que llamar a .next().value
, ¿verdad?
… Pero tenga en cuenta: el ejemplo anterior muestra 1
, luego2
, y eso es todo. ¡No muestra 3
!
Es porque la iteración for..of
ignora el último value
, cuando done: true
. Entonces, si queremos que todos los resultados se muestren con for..of
, debemos devolverlos con yield
:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, luego 2, luego 3
}
Como los generadores son iterables, podemos llamar a todas las funciones relacionadas, p. Ej. la sintaxis de propagación ...
:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let sequence = [0, ...generateSequence()];
alert(sequence); // 0, 1, 2, 3
En el código anterior, ... generateSequence ()
convierte el objeto generador iterable en un array de elementos (lea más sobre la sintaxis de propagación en el capítulo Parámetros Rest y operador Spread)
Usando generadores para iterables
Hace algún tiempo, en el capítulo Iterables creamos un objeto iterable range
que devuelve valores from..to
.
Recordemos el código aquí:
let range = {
from: 1,
to: 5,
// for..of range llama a este método una vez al principio
[Symbol.iterator]() {
// ...devuelve el objeto iterador:
// en adelante, for..of funciona solo con ese objeto, solicitándole los siguientes valores
return {
current: this.from,
last: this.to,
// next() es llamado en cada iteración por el bucle for..of
next() {
// debería devolver el valor como un objeto {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
// iteración sobre range devuelve números desde range.from a range.to
alert([...range]); // 1,2,3,4,5
Podemos utilizar una función generadora para la iteración proporcionándola como Symbol.iterator
.
Este es el mismo range
, pero mucho más compacto:
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // una taquigrafía para [Symbol.iterator]: function*()
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
alert( [...range] ); // 1,2,3,4,5
Eso funciona, porque range[Symbol.iterator]()
ahora devuelve un generador, y los métodos de generador son exactamente lo que espera for..of
:
- tiene un método
.next()
- que devuelve valores en la forma
{value: ..., done: true/false}
Eso no es una coincidencia, por supuesto. Los generadores se agregaron al lenguaje JavaScript con los iteradores en mente, para implementarlos fácilmente.
La variante con un generador es mucho más concisa que el código iterable original de range
y mantiene la misma funcionalidad.
En los ejemplos anteriores, generamos secuencias finitas, pero también podemos hacer un generador que produzca valores para siempre. Por ejemplo, una secuencia interminable de números pseudoaleatorios.
Eso seguramente requeriría un break
(o return
) en for..of
sobre dicho generador. De lo contrario, el bucle se repetiría para siempre y se colgaría.
Composición del generador
La composición del generador es una característica especial de los generadores que permite “incrustar” generadores entre sí de forma transparente.
Por ejemplo, tenemos una función que genera una secuencia de números:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
Ahora nos gustaría reutilizarlo para generar una secuencia más compleja:
- primero, dígitos
0..9
(con códigos de caracteres 48…57), - seguido de letras mayúsculas del alfabeto
A..Z
(códigos de caracteres 65…90) - seguido de letras del alfabeto en minúscula
a..z
(códigos de carácter 97…122)
Podemos usar esta secuencia, p. Ej. para crear contraseñas seleccionando caracteres de él (también podría agregar caracteres de sintaxis), pero vamos a generarlo primero.
En una función regular, para combinar los resultados de muchas otras funciones, las llamamos, almacenamos los resultados y luego nos unimos al final.
Para los generadores, hay una sintaxis especial yield*
para “incrustar” (componer) un generador en otro.
El generador compuesto:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);
// A..Z
yield* generateSequence(65, 90);
// a..z
yield* generateSequence(97, 122);
}
let str = '';
for(let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
La directiva yield*
delega la ejecución a otro generador. Este término significa que yield* gen
itera sobre el generador gen
y reenvía de forma transparente sus yields al exterior. Como si los valores fueran proporcionados por el generador externo.
El resultado es el mismo que si insertamos el código de los generadores anidados:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generateAlphaNum() {
// yield* generateSequence(48, 57);
for (let i = 48; i <= 57; i++) yield i;
// yield* generateSequence(65, 90);
for (let i = 65; i <= 90; i++) yield i;
// yield* generateSequence(97, 122);
for (let i = 97; i <= 122; i++) yield i;
}
let str = '';
for(let code of generateAlphaNum()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
La composición de un generador es una forma natural de insertar un flujo de un generador en otro. No usa memoria adicional para almacenar resultados intermedios.
“yield” es una calle de doble sentido
Hasta este momento, los generadores eran similares a los objetos iterables, con una sintaxis especial para generar valores. Pero de hecho son mucho más potentes y flexibles.
Eso es porque yield
es una calle de doble sentido: no solo devuelve el resultado al exterior, sino que también puede pasar el valor dentro del generador.
Para hacerlo, deberíamos llamar a generator.next (arg)
, con un argumento. Ese argumento se convierte en el resultado de yield
.
Veamos un ejemplo:
function* gen() {
// Pasar una pregunta al código externo y esperar una respuesta
let result = yield "2 + 2 = ?"; // (*)
alert(result);
}
let generator = gen();
let question = generator.next().value; // <-- yield devuelve el valor
generator.next(4); // --> pasar el resultado al generador
- La primera llamada a
generator.next ()
debe hacerse siempre sin un argumento (el argumento se ignora si se pasa). Inicia la ejecución y devuelve el resultado del primeryield "2 + 2 = ?"
. En este punto, el generador detiene la ejecución, mientras permanece en la línea(*)
. - Luego, como se muestra en la imagen de arriba, el resultado de
yield
entra en la variablequestion
en el código de llamada. - En
generator.next(4)
, el generador se reanuda y4
entra como resultado:let result = 4
.
Tenga en cuenta que el código externo no tiene que llamar inmediatamente a next(4)
. Puede que lleve algún tiempo. Eso no es un problema: el generador esperará.
Por ejemplo:
// reanudar el generador después de algún tiempo
setTimeout(() => generator.next(4), 1000);
Como podemos ver, a diferencia de las funciones regulares, un generador y el código de llamada pueden intercambiar resultados pasando valores en next/yield
.
Para hacer las cosas más obvias, aquí hay otro ejemplo, con más llamadas:
function* gen() {
let ask1 = yield "2 + 2 = ?";
alert(ask1); // 4
let ask2 = yield "3 * 3 = ?"
alert(ask2); // 9
}
let generator = gen();
alert( generator.next().value ); // "2 + 2 = ?"
alert( generator.next(4).value ); // "3 * 3 = ?"
alert( generator.next(9).done ); // true
Imagen de la ejecución:
- El primer
.next()
inicia la ejecución … Llega al primeryield
. - El resultado se devuelve al código externo.
- El segundo
.next(4)
pasa4
de nuevo al generador como resultado del primeryield
y reanuda la ejecución. - …Alcanza el segundo
yield
, que se convierte en el resultado de la llamada del generador. - El tercer
next(9)
pasa9
al generador como resultado del segundoyield
y reanuda la ejecución que llega al final de la función, así quedone: true
.
Es como un juego de “ping-pong”. Cada next(value)
(excluyendo el primero) pasa un valor al generador, que se convierte en el resultado del yield
actual, y luego recupera el resultado del siguiente yield
.
generator.throw
Como observamos en los ejemplos anteriores, el código externo puede pasar un valor al generador, como resultado de yield
.
…Pero también puede iniciar (lanzar) un error allí. Eso es natural, ya que un error es una especie de resultado.
Para pasar un error a un yield
, deberíamos llamar a generator.throw(err)
. En ese caso, el err
se coloca en la línea con ese yield
.
Por ejemplo, aquí el yield de "2 + 2 = ?"
conduce a un error:
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)
alert("La ejecución no llega aquí, porque la excepción se lanza arriba");
} catch(e) {
alert(e); // muestra el error
}
}
let generator = gen();
let question = generator.next().value;
generator.throw(new Error("The answer is not found in my database")); // (2)
El error, arrojado al generador en la línea (2)
conduce a una excepción en la línea (1)
con yield
. En el ejemplo anterior, try..catch
lo captura y lo muestra.
Si no lo detectamos, al igual que cualquier excepción, “cae” del generador en el código de llamada.
La línea actual del código de llamada es la línea con generator.throw
, etiquetada como (2)
. Entonces podemos atraparlo aquí, así:
function* generate() {
let result = yield "2 + 2 = ?"; // Error en esta linea
}
let generator = generate();
let question = generator.next().value;
try {
generator.throw(new Error("La respuesta no se encuentra en mi base de datos"));
} catch(e) {
alert(e); // mostrar el error
}
Si no detectamos el error allí, entonces, como de costumbre, pasa al código de llamada externo (si lo hay) y, si no se detecta, mata el script.
generator.return
generator.return(value)
detiene la ejecución de generator y devuelve el valor value
dado.
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
g.next(); // { value: 1, done: false }
g.return('foo'); // { value: "foo", done: true }
g.next(); // { value: undefined, done: true }
Si volvemos a usar generator.return()
en un generator finalizado, devolverá ese valor nuevamente (MDN).
No lo usamos a menudo, ya que la mayor parte del tiempo queremos todos los valores, pero puede ser útil cuando queremos detener el generador en una condición específica.
Resumen
- Los generadores son creados por funciones generadoras
function* f(…) {…}
. - Dentro de los generadores (solo) existe un operador
yield
. - El código externo y el generador pueden intercambiar resultados a través de llamadas
next/yield
.
En JavaScript moderno, los generadores rara vez se utilizan. Pero a veces son útiles, porque la capacidad de una función para intercambiar datos con el código de llamada durante la ejecución es bastante única. Y, seguramente, son geniales para hacer objetos iterables.
Además, en el próximo capítulo aprenderemos los generadores asíncronos, que se utilizan para leer flujos de datos generados asincrónicamente (por ejemplo, recuperaciones paginadas a través de una red) en bucles for await ... of
.
En la programación web, a menudo trabajamos con datos transmitidos, por lo que ese es otro caso de uso muy importante.
Comentarios
<code>
, para varias líneas – envolverlas en la etiqueta<pre>
, para más de 10 líneas – utilice una entorno controlado (sandbox) (plnkr, jsbin, codepen…)