Descubriendo las Funciones Generadoras en JavaScript

Abdiel Martinez |

¿Te gustaría saber cómo JavaScript puede pausar y reanudar la ejecución de tu código? Acompáñame a descubrirlo

Esta es una de esas características de JavaScript que usualmente pasan desapercibidas. Conocerlas, sin embargo, te ayudará a profundizar en el lenguaje y todas sus posibilidades. ¿De qué estamos hablando?

¿En qué consisten?

Este tipo de funciones tienen la capacidad de pausar y reanudar su ejecución posteriormente, lo que las hace ideales para trabajar con conjuntos de datos grandes y potencialmente infinitos.

Una función generadora se define utilizando la palabra clave function* en lugar de function. Dentro de la función generadora, se utiliza la palabra clave yield para pausar la ejecución y devolver un valor al iterador. Cuando se llama nuevamente a la función generadora, la ejecución se reanuda desde el punto donde se pausó.

Las funciones generadoras pueden ser utilizadas en combinación con la sintaxis for...of para iterar de manera sencilla sobre los valores generados.

Las funciones generadoras no pueden ser definidas mediante el uso de Arrow functions.

¿Qué genera una función generadora?

Al ejecutar una función generadora, su código no se ejecuta inmediatamente, en su lugar, devuelve un objeto iterador llamado Generator.

El objeto Generator tiene 3 métodos que podemos llamar estos son:

  • next que retorna un objeto con las propiedades: value, que corresponde al valor de la expresión yield y la propiedad done que nos indica si hemos consumido el ultimo valor de la iteración.
  • return que retorna el valor que le pasamos como argumento y finaliza el generador.
  • throw que lanza como error el valor que enviemos como argumento y también finaliza el generador

Imagina que tenemos la siguiente función generadora:

function* getGenerator() {
  yield "ejemplo 1";
  yield "ejemplo 2";
}

const generator = getGenerator();

console.log(generator.next());
// { value: 'ejemplo 1', done: false }

console.log(generator.return("valor de retorno"));
// { value: 'valor de retorno', done: true }

console.log(generator.throw("mensaje de error"));
// Error: mensaje de error

Para utilizar el generador con un for-of, se haría de la siguiente manera:

for (const value of getGenerator()) {
  console.log(value);
  // 'ejemplo 1'
  // 'ejemplo 2'
}

Si quisieras utilizar los métodos de los arrays para los resultados del generador, sería tan sencillo como usar la función Array.from() de la siguiente manera.

Array.from(getGenerator()).map(item => item.toUpperCase());
// [ 'EJEMPLO 1', 'EJEMPLO 2' ]

Uso avanzado

  • El método next del objeto generador puede recibir un parámetro y así cambiar el estado interno de la función generadora. Ejemplo:
function* getGenerator() {
  const value = yield "ejemplo 1";
  yield value || "ejemplo 2";
}

console.log(generator.next());
// { value: 'ejemplo 1', done: false }

console.log(generator.next("valor externo"));
// { value: 'valor externo', done: false }

En este caso, al realizar la llamada al método next por segunda vez, pasamos un valor que es recibido dentro de la función generadora a través de la expresión yield previa.

  • Otro uso avanzado es delegar la iteración a otro generador u objeto iterable mediante el uso de la expresión yield*.

Imagina el siguiente caso sin delegar la iteración:

function* getGenerator() {
  yield "ejemplo 1";
  yield ["valor 1", "valor 2"];
}

for (const value of getGenerator()) {
  console.log(value);
  // 'ejemplo 1'  --> primera iteracion
  // [ 'valor 1', 'valor 2' ]  --> segunda iteracion
}

Mira como cambia si delegamos la iteración con el uso de yield*:

function* getGenerator() {
  yield "ejemplo 1";
  yield* ["valor 1", "valor 2"];
}

for (const value of getGenerator()) {
  console.log(value);
  // 'ejemplo 1'  --> primera iteracion
  // 'valor 1'  --> segunda iteracion
  // 'valor 2'  --> tercera iteracion
}

Al utilizar yield*, podemos recorrer el array como parte de las iteraciones del generador. También podemos hacer lo mismo al devolver con la expresión yield* otro generador, un string, un Map, un WeakMap, un Set o un WeakSet.

Caso práctico de uso

Imagina que necesitas solicitar datos de una API que utiliza paginación. Podrías implementar un enfoque como el siguiente:

async function* getUserData() {
  let page = 1;
  while (true) {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/todos/${page}`
    );
    yield response.json();

    page++;
  }
}

(async () => {
  const request = getUserData();
  console.log(await request.next());
  /* { 
  value: {
    userId: 1,
    id: 1,
    title: 'delectus aut autem',
    completed: false
  },
  done: false
} */

  console.log(await request.next());
  /* {
  value: {
    userId: 1,
    id: 2,
    title: 'quis ut nam facilis et officia qui',
    completed: false
  },
  done: false
} */
})();

Implementándolo de esta manera por cada iteración realizada se realizaría una request a la API aumentando la pagina solicitada en cada iteración.

¿Qué opinas de las funciones generadoras? ¿Qué otros casos prácticos encuentras para su uso? Cuéntamelo en los comentarios.

Fuentes