Iteradores y generadores

Procesar cada uno de los elementos en una colección es una operación muy común. JavaScript proporciona diversas formas de iterar sobre una colección, desde simples bucles for hasta métodos como map() y filter(). Los iteradores y los generadores traen el concepto de iteración al centro del lenguaje y proporcionan un mecanismo para personalizar el comportamiento de los bucles for...of.

Para más información, véase:

Iteradores

En JavaScript, un iterador es un objeto que permite recorrer una colección y devolver un valor al terminar.

Específicamente, un iterador es un objeto que implementa el protocolo de iteración a través del método next(), el cual devuelve un objeto con dos propiedades:

value

El siguiente valor en la secuencia de iteración.

done

Es true si el último valor en la secuencia ya ha sido consumido. Si value está presente junto con done, es el valor de retorno del iterador.

Un iterador se considera ya terminado/finalizado cuando la invocación de next() regresa un objeto donde la propiedad done es verdadero.

Una vez creado, un objeto iterador puede utilizarse explícitamente llamando repetidamente al método next().

js
function crearIterador(arreglo) {
  var siguienteIndice = 0;

  return {
    next: function () {
      return siguienteIndice < arreglo.length
        ? { value: arreglo[siguienteIndice++], done: false }
        : { done: true };
    },
  };
}

Una vez inicializado, se puede invocar al método next() para acceder a las parejas llave-valor del objeto en cuestión:

js
var it = crearIterador(["yo", "ya"]);
console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done); // true

Generadores

Aunque los iteradores personalizados son una herramienta útil, su creación require una programación meticulosa ya que necesitan mantener su estado interno explícitamente. Los generadores son una alternativa poderosa: permiten definir un algoritmo iterativo al escribir una sola función que puede mantener su propio estado.

Una función generadora (constructor GeneratorFunction) es un tipo especial de función que sirve como una fábrica de iteradores. Cuando se ejecuta, regresa un nuevo objeto Generador. Una función se convierte en una Función Generadora si utiliza la sintáxis function*.

js
function* hacedorIds() {
  var indice = 0;
  while (true) {
    yield indice++;
  }
}

var gen = hacedorIds();

console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
// ...

Iterables

Un objeto es iterable si define cómo se itera. Un ejemplo son los valores que se iteran en un bucle for...of. Algunos tipos integrados de datos, como Array o Map (en-US), tienen una forma de iteración ya definida, mientras que otras no (como Object).

Con el fin de ser iterable, un objeto debe implementar el método @@iterator. Esto quiere decir que dicho objeto (o alguno en su cadena de prototipos) debe tener una propiedad definida usando la llave Symbol.iterator. Esta función debería regresar un nuevo iterador en cada invocación, pero no es obligatorio.

Iterables definidos por el usuario

Podemos hacer nuestros propios objetos iterables de este modo:

js
var miIterable = {};
miIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

for (let valor of miIterable) {
  console.log(valor);
}
// 1
// 2
// 3

// ó

[...miIterable]; // [1, 2, 3]

Iterables integrados

String, Array, Objetos_globales/TypedArray, Map (en-US) y Set son iterables ya integrados, porque todos sus objetos prototipo tienen un método definido con la llave Symbol.iterator.

Sintaxis que esperan objetos iterables

Algunas sentencias y expresiones esperan objetos iterables, por ejemplo los bucles for-of, el operador de propagación, yield*, y la asignación por desestructuración.

js
for (let valor of ["a", "b", "c"]) {
  console.log(valor);
}
// "a"
// "b"
// "c"

[..."abc"]; // ["a", "b", "c"]

function* gen() {
  yield* ["a", "b", "c"];
}

gen().next(); // { value:"a", done:false }

[a, b, c] = new Set(["a", "b", "c"]);
a; // "a"

Generadores avanzados

Los generadores calculan los valores devueltos bajo demanda, lo que les permite representar eficientemente secuencias que son costosas de calcular, o incluso secuencias infinitas como se explicó anteriormente.

El método next() también acepta un valor que puede ser utilizado para modificar el estado interno del generador. El valor recibido por next() es utilizado como si fuera el resultado de la iteración anterior (último valor entregado por yield) el cual detuvo al generador.

A continuación se muestra un generador de Fibonacci usando next(x) para reiniciar la secuencia:

js
function* fibonacci() {
  var fn1 = 1;
  var fn2 = 1;
  while (true) {
    var actual = fn2;
    fn2 = fn1;
    fn1 = fn1 + actual;
    var reset = yield actual;
    if (reset) {
      fn1 = 1;
      fn2 = 1;
    }
  }
}

var secuencia = fibonacci();
console.log(secuencia.next().value); // 1
console.log(secuencia.next().value); // 1
console.log(secuencia.next().value); // 2
console.log(secuencia.next().value); // 3
console.log(secuencia.next().value); // 5
console.log(secuencia.next().value); // 8
console.log(secuencia.next().value); // 13
console.log(secuencia.next(true).value); // 1
console.log(secuencia.next().value); // 1
console.log(secuencia.next().value); // 2
console.log(secuencia.next().value); // 3

Es posible forzar a un generador a lanzar una excepción cuando se invoca al método throw() y se pasa el valor de excepción a lanzar. Esta excepción será lanzada desde el contexto actual suspendido del generador, como si en vez del estado suspendido actualmente de yield se tuviera una sentencia throw valor.

Si la excepción no es atrapada dentro del generador, se propagará a la invocación de throw(), y las siguientes llamadas a next() tendrán a la propiedad done en verdadero.

Los generadores tienen un método return(valor) que regresa el valor enviado y finalizan al generador.