¡Esta es una revisión vieja del documento!
Tabla de Contenidos
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
objes 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:
- Crear una función, llamada
Symbol.iteratorque… - devuelva un objeto, que tenga tan solo una…
- función, llamada
next. Esta función debe… - devolver un objeto con dos propiedades:
- Una de las cuales debe llamarse
value - y la otra debe llamarse
doney 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); }
