Como ya habremos visto, a la hora de trabajar en nuestro código tendremos situaciones donde buscamos recorrer ciertas estructuras de datos. Imagina la siguiente estructura de tipo
const data = ["One", "Two", "Three", "Four", "Five"];
Necesitamos una forma de ir, paso a paso, transitando por cada uno de sus elementos y mostrarlos o utilizarlos para algún fin. Esto se puede hacer de múltiples formas:
- 1️⃣ Sin iterador: Usamos un
para iterar. - 2️⃣ Iterador implícito: Usamos un iterador (aunque no lo sabemos).
- 3️⃣ Iterador explícito: Usamos un iterador de forma manual.
¿Qué es un iterador?
Un iterador es un elemento especial de programación que permite recorrer los elementos de una estructura en un cierto orden, generalmente, de forma secuencial. Veamos algunos ejemplos para recorrer elementos sin y con iteradores.
Sin usar iteradores
En Javascript, normalmente creamos bucles con variables simples que actúan de iterador. Técnicamente, esto no son iteradores, pero son sencillos, simples de entender y cumplen su función: sirven para iterar.
const data = ["One", "Two", "Three", "Four", "Five"];
for (let i = 0; i < data.length; i++) {
console.log(data[i]);
}
Como se puede ver, estamos creando una variable i
que será un data
para ir avanzando a lo largo de sus elementos.
Usando iteradores (sin saberlo)
En ciertos casos es posible utilizar iteradores de forma implícita, es decir, los estás utilizando, pero muchas veces no eres consciente de ello. Métodos de array como .forEach()
, o los bucles for..in
o for..of
utilizan iteradores implícitos:
const data = [5, 4, 3, 2, 1];
// Array functions
data.forEach(number => console.log(number));
// for..of
for (let number of data) {
console.log(number);
}
Esto funciona porque existe un Symbol.iterator
en la estructura. Si escribimos data[Symbol.iterator]
en la consola, veremos que nos devuelve una función. Esa función es el iterador, que veremos en la siguiente pestaña.
Usando iteradores
Como mencionamos antes, las estructuras que sean iterables tendrán definido un Symbol.iterator
que contendrá una función.
Observa el siguiente fragmento de código:
const data = [5, 4, 3, 2, 1];
const iterator = data.values(); // Array iterator {}
Si ejecutamos data.values()
nos devolverá un iterador del array. De esta forma, podremos ejecutarlo para ir iterando los elementos.
Obtener el iterador de
mediante data.values()
es sólo una versión más bonita para acceder adata[Symbol.iterator]()
. Hacen exactamente lo mismo.
Este iterador contiene un método o función llamado .next()
, que al ejecutarlo nos devolverá una estructura con dos propiedades:
- 1️⃣
value
El valor devuelto por el iterador - 2️⃣
done
Unque nos dice si terminó de iterar
Observa como iteraríamos paso a paso la estructura, ejecutando next()
:
const data = [5, 4, 3, 2, 1];
const iterator = data.values(); // Array iterator {}
// Iteramos manualmente
iterator.next() // { value: 5, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 1, done: false }
iterator.next() // { value: undefined, done: true }
Una vez, nos devuelve done: true
, ya hemos terminado de iterar y no nos devolverá más valores por mucho que ejecutemos .next()
.
Sin embargo, también podemos iterar de forma automática. Por ejemplo, si utilizamos el operador spread escribiendo ...
y reestructurandolo en un
const data = [5, 4, 3, 2, 1];
const iterator = data.values(); // Array iterator {}
// Iteramos automáticamente
[...iterator] // [5, 4, 3, 2, 1]
En el caso de los
Estructuras no iterables
No todas las estructuras de datos son iterables por defecto. Por ejemplo, un
Veamos algunos ejemplos.
Sin iteradores
En este caso, hemos creado un from
y to
:
const data = {
from: 1,
to: 5
}
// Intentamos iterar
[...data]
Si intentamos iterar esta estructura, reestructurándola con ...
y envolviéndolo en un
Uncaught TypeError: data is not iterable
Esto ocurre porque los objetos no son iterables por defecto.
Con iteradores
Sin embargo, podemos crear un objeto y al igual que tiene una propiedad from
y to
, añadirle una propiedad [Symbol.iterator]
que contenga una función personalizada que defina el orden en que se iterará el objeto:
const data = {
from: 1,
to: 5,
[Symbol.iterator]() {
/* ... */
}
}
Vamos a implementar esa función para personalizar el iterador:
- 1️⃣ Creamos las variables
value
yend
que toman los valores inicial y final. - 2️⃣ El iterador debe devolver un objeto con la función
next()
. - 3️⃣ La función
next()
debe devolver un objeto convalue
ydone
. - 4️⃣ En nuestro caso, devolvemos el valor de
value
incrementado ydone
a falso. - 5️⃣ Si
value
llega aend
, entonces devolvemosdone
atrue
.
const data = {
from: 1,
to: 5,
[Symbol.iterator]() {
let value = this.from;
let end = this.to;
return {
next() {
if (value > end) return { done: true }
return {
value: value++,
done: false
}
}
}
}
}
// Iteramos
[...data] // [1, 2, 3, 4, 5]
Ahora, si accedemos a [...data]
comprobarás que nuestra estructura si que es iterable.
Manualmente
Recuerda que en el ejemplo anterior estoy usando [...data]
para hacerlo rápidamente, pero podríamos usar la iteración de forma manual, ejecutando en un bucle la función .next()
del iterador:
const iterator = data.values(); // Array iterator {}
iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: 2, done: false }
iterator.next(); // { value: 3, done: false }
iterator.next(); // { value: 4, done: false }
iterator.next(); // { value: 5, done: false }
iterator.next(); // { value: undefined, done: true }
Aunque todo esto parece muy teórico y poco práctico, piensa que mediante la personalización de iteradores se pueden crear estructuras de datos que sean iterables por defecto, simplificando mucho el trabajo con estas estructuras.
Las ventajas principales de utilizar iteradores serían:
- 💖 Más limpio: Personalizas el iterador y recorrer la estructura luego es más fácil.
- 💖 Puedes hacer cualquier estructura iterable (aunque no sea secuencial).
- 💖 Es menos propenso a errores.
- 💖 Permite usar generadores. (Ver más adelante)
- 💖 Pausable.
- 💔 El control de los índices es mayor con los iteradores numéricos.