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:truesi 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
yieldentra en la variablequestionen el código de llamada. - En
generator.next(4), el generador se reanuda y4entra 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)pasa4de nuevo al generador como resultado del primeryieldy reanuda la ejecución. - â¦Alcanza el segundo
yield, que se convierte en el resultado de la llamada del generador. - El tercer
next(9)pasa9al generador como resultado del segundoyieldy 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â¦)