Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:programacion_avanzada_javascript:iteradores_generadores

¡Esta es una revisión vieja del documento!


Iteradores y generadores

Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.

Introducción

Los iteradores son un concepto totalmente nuevo en ECMAScript. Básicamente un iterador define un mecanismo que permite iterar (o recorrer) por los valores de un objeto. Cualquier objeto que proporcione un iterador, se conoce como un objeto iterable y podremos iterar por sus valores.

Los arrays, y los tipos Map y Set son iterables (ya que proporcionan su propio iterador). Pero los objetos que creemos nosotros pueden serlo también.

En este módulo vamos a ver cómo iterar a través de objetos iterables (bucle for-of) y cómo definir iteradores. Al final iremos un paso más allá y hablaremos de los generadores, un mecanismo que permite definir iteradores de forma muy sencilla.

DEMO: Tipos de bucles

Veremos los distintos tipos de bucles disponibles en JavaScript.

var data = [1, 10, 100, 1000];

Utilizando un bucle for:

let suma = 0;
 
for (let idx = 0; idx < data.length; idx++) {
    suma += data[idx];
}

Utilizando en bucle for-in:

let suma = 0;
 
for (let v in data) {
    suma += v;
}

Si vemos el resultado de suma será 00123. La razón de este resultado es que el bucle for-in no itera sobre los valores de un objeto sino sobre sus claves / índices. Podemos verlo con:

let suma = 0;
 
for (let v in data) {
    console.log("v: ", v, typeof(v));
    suma += v;
}

Las claves de los arrays, los índices, siempre son cadenas.

En ECMAScript 2015 tenemos el bucle for-of que sí itera sobre los valores:

let suma = 0;
 
for (let v of data) {
    suma += v;
}

Si queremos usar for-of para iterar sobre un objeto, este objeto debe ser iterable.

Iteradores

Un iterador es un objeto que permite iterar sobre otro objeto. En el caso de ECMAScript el iterador se obtiene siempre a partir del objeto que se vaya a iterar. Para ello se usa una función con un nombre específico, un símbolo accesible a través de Symbol.iterator. Por lo tanto un objeto será iterable si define la función Symbol.iterator que devuelve un iterador.

Es importante notar que la función Symbol.iterator no es el iterador en sí mismo. Esta función es la que permite obtener el iterador. El iterador realmente es un objeto con un solo método (next) que permite iterar sobre los valores.

Cada iterador permite iterar por todos los valores una sola vez y son “forward only (es decir solo pueden ir obteniendo los elementos de uno a uno y solo hacia adelante).

Cualquier objeto que tenga un iterador, decimos que es iterable, y tenemos dos maneras básicas de iterar sobre él:

  • La primera es el uso del bucle for…of. Este bucle funciona no solo en arrays, sino de hecho con cualquier objeto iterable.
  • La otra opción es el uso del operador spread. Si el objeto obj es iterable entonces podemos obtener un array con todos los valores de dicho objeto de la siguiente forma:
var values = [...obj]

Vamos a verlo…

Creando iteradores

a creación de un iterador es realmente sencilla aunque un poco “tediosa” por el código a usar. Básicamente para crear un iterador hay que:

  1. Crear una función, llamada Symbol.iterator que…
  2. devuelva un objeto, que tenga tan solo una…
  3. función, llamada next. Esta función debe…
  4. devolver un objeto con dos propiedades:
  5. Una de las cuales debe llamarse value
  6. y la otra debe llamarse done y es un booleano.

En resumen, el código base para crear un iterador es el siguiente:

[Symbol.iterator]() {
    return {
        next() {
            return { done: true, value: xxx }
        }
    }
}

IMPORTANTE: Observa que el nombre de la función (Symbol.iterator) es un símbolo, no una cadena. De ahí que debamos colocarlo entre corchetes.

Entendiendo el código

Para iterar sobre un objeto se requiere un iterador. El iterador se obtiene una sola vez cuando se empieza a iterar por el objeto. El iterador se obtiene a través de la función Symbol.iterator.

Por supuesto, si se empiezan varias iteraciones (p. ej. se usan varios bucles for…of) se obtienen varios iteradores (cada iterador nos permite iterar una vez por todos los valores del objeto).

Una vez tenemos un iterador debemos obtener valores. Por cada valor a obtener se llama al método next del iterador. Voy a recalcarlo porque es muy importante: solo se llama al método next cuando se requiere un nuevo elemento. Es por eso que decimos que los iteradores usan “evaluación perezosa” (lazy evaluation). Si solo necesitamos dos elementos del iterador llamaremos solo dos veces al método next.

El método next nos devuelve un objeto con las propiedades done y value. La que manda es la propiedad done. Si vale true, entonces el valor de value es ignorado porque se asume que ya no hay valores sobre los que iterar.

Así, un iterador que devuelva N valores, si iteramos por todos ellos, el método next será llamado N+1 veces. En las N primeras debe devolver los N valores (y done a false) y en la última llamada debe devolver done a true (y es irrelevante lo que devuelva en value).

DEMO: Creando un iterador

var obj = {
    // Función que permitirá iterar
    [Symbol.iterator]() {
        let idx = 0;
        return {
            next() {
                idx++;
                return {
                    value: 10,
                    done: true
                }
            }
        }
    }
}

Ahora podemos iterar:

for (let v of obj) {
    console.log(v);
}

Veremos que no devuelve nada porque al poner done como true ignora el valor.

var obj = {
    // Función que permitirá iterar
    [Symbol.iterator]() {
        let idx = 0;
        return {
            next() {
                idx++;
                return {
                    value: 10,
                    done: idx > 1
                }
            }
        }
    }
}
for (let v of obj) {
    console.log(v);
}

Ahora sí mostrará el 10.

Pongamos varios console.log para ver cómo se ejecuta:

var obj = {
    // Función que permitirá iterar
    [Symbol.iterator]() {
        console.log("obteniendo iterador");
        let idx = 0;
        return {
            next() {
                console.log("obteniendo valor");
                idx++;
                return {
                    value: 10,
                    done: idx > 1
                }
            }
        }
    }
}
console.log("inicio");
 
for (let v of obj) {
    console.log(v);
}
 
console.log("fin");
 
// -> inicio
// -> obteniendo iterador
// -> obteniendo valor
// -> 10
// -> obteniendo valor
// -> fin

DEMO: Un iterador un poco más realista

var repo = {
    _data: [],
    add(value) {
        this._data.push(value);
    },
    [Symbol.iterator]() {
 
        let idx = 0;
        var self = this;
 
        return {
            next() {
                let current = self._data[idx];
                idx++;
 
                return {
                    value: current,
                    done: idx > self._data.length
                }
            }
        }
    }
}

Probemos:

repo.add({v: 40});
repo.add({v: 60});
 
for (let x of repo) {
    console.log(x);
}
 
// Salida:
// Object { v: 40 } 
// Object { v: 60 } 

DEMO: Un iterador un poco más realista (II)

Vamos a completar el ejemplo anterior haciendo el array data sea privado y no un miembro de repo. Para ello necesitamos una función por encima que nos dé un ámbito:

const repo =  (() => 
 
    let _data = [];
 
    var repo = {
        add(value) {
            this._data.push(value);
       },
       [Symbol.iterator]() {
 
            let idx = 0;
            var self = this;
 
            return {
                next() {
                    let current = _data[idx];
                    idx++;
 
                    return {
                       value: current,
                       done: idx > _data.length
                    }
                }
            }
        }
    }
    return repo;
})();

Probamos:

repo.add({v: 42});
repo.add({v: 60});
 
for (let x of repo) {
    console.log(x);
}

Iteradores infinitos

Dado que los iteradores se evalúan perezosamente podemos construir iteradores que nunca terminen. Para ello es tan sencillo como hacer un iterador que nunca ponga a true la propiedad done:

let fibonacci = {
  [Symbol.iterator]() {
      let pre = 0, cur = 1;
      return {
          next() {
              [pre, cur] = [cur, pre + cur];
              return { done: false, value: cur }
          }
      }
  }
}

El objeto fibonacci define un iterador, que nunca termina, que devuelve la sucesión de Fibonacci. Por supuesto si iteramos sobre este iterador debemos parar la iteración en algún momento o si no tendremos un bucle infinito (o algún otro error como falta de memoria si estamos guardando los valores en un array):

for (var n of fibonacci) {
   if (n > 1000)
        break;
    console.log(n);
}

Este código imprime por la consola todos los valores de la sucesión de Fibonacci inferiores o iguales a 1000. Cuando encontramos uno superior, terminamos la iteración con break. A pesar de que el iterador potencialmente pueda devolver infinitos valores, al ser evaluados de forma perezosa, no hay ningún problema.

Generadores

Creando generadores

DEMO: Creando un generador

Generadores como objetos

DEMO: Generadores como objetos

Ejercicios propuestos

informatica/programacion/cursos/programacion_avanzada_javascript/iteradores_generadores.1729173557.txt.gz · Última modificación: por tempwin