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().

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:

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*.

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, 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:

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 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.

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:

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.