Kodokon kodokon.com

Iteradores, generadores y protocolos de iteración

Domina los protocolos detrás de for...of, la propagación y la desestructuración, y luego usa generadores para producir secuencias perezosas.

9 min · 3 preguntas

Abrir esta lección en Kodokon

Dos protocolos distintos rigen la iteración. Un objeto es iterable si expone un método bajo la clave Symbol.iterator; ese método debe devolver un iterador, es decir, un objeto con un método next() que devuelve { value, done }. Todo lo que consume secuencias - for...of, el operador de propagación ..., la desestructuración, Array.from, Promise.all - pasa por estos protocolos. Por lo tanto, puedes hacer que cualquier objeto sea compatible.

JAVASCRIPT
const range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
};

console.log([...range]); // [1, 2, 3]
Un iterable escrito a mano, sin clase ni generador.

Escribir iteradores a mano es verboso. Los generadores (function*) producen un iterador automáticamente: cada yield suspende la ejecución y preserva el estado local de la función. Esta evaluación perezosa te permite modelar secuencias infinitas, siempre que un consumidor limite el número de valores solicitados.

JAVASCRIPT
function* naturals() {
  let n = 0;
  while (true) yield n++;
}

function* take(iterable, count) {
  let index = 0;
  for (const value of iterable) {
    if (index >= count) return;
    yield value;
    index += 1;
  }
}

console.log([...take(naturals(), 4)]); // [0, 1, 2, 3]
Una secuencia infinita consumida sin bloquear nunca.

Los iteradores de generador también exponen return() y throw(). Un detalle de la especificación que a menudo pasa desapercibido: cuando abandonas un bucle for...of con break, return o una excepción, el motor llama automáticamente a iterator.return(). En un generador, eso ejecuta cualquier bloque finally pendiente - el lugar ideal para liberar un recurso (un archivo, una conexión, un bloqueo).

JAVASCRIPT
function* readLines() {
  try {
    yield "line 1";
    yield "line 2";
  } finally {
    console.log("cleanup");
  }
}

for (const line of readLines()) {
  console.log(line);
  break; // triggers return(), hence the finally
}
// line 1, cleanup
break llama a return(): el bloque finally se ejecuta.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Qué hace que un objeto sea iterable en el sentido del protocolo?
    • Tener una propiedad length e índices numéricos
    • Exponer un método Symbol.iterator que devuelve un objeto con next()
    • Heredar de Array.prototype
    • Tener un método next() directamente
  2. ¿Qué ocurre cuando sales de un bucle for...of con break mientras consume un generador?
    • Nada especial, el generador queda suspendido en el último yield
    • El motor llama a throw() sobre el iterador
    • El motor llama a return(), que ejecuta los bloques finally del generador
    • El generador se reinicia desde el principio en la siguiente iteración
  3. ¿Por qué puedes trabajar con un generador infinito sin bloquear el programa?
    • El motor limita automáticamente el número de iteraciones
    • Los generadores se ejecutan en un hilo separado
    • La evaluación es perezosa: cada valor solo se calcula cuando se llama a next()
    • Los valores se precalculan y se almacenan en caché por el motor