JavaScript es compatible con un protocolo mediante el cual las estructuras de control como, for…of y el operador de propagación … pueden utilizar objetos, como arreglos, para recorrer los datos de forma secuencial. Esto se conoce como iteradores y las estructuras de datos que soportan esta funcionalidad se denominan iterables. Si bien JavaScript proporciona mapas, arreglos y conjuntos con una propiedad iterable desde el principio, los objetos simples no tienen esto de forma predeterminada.
Los iterables son estructuras de datos que proporcionan un mecanismo para permitir que otros consumidores de datos accedan públicamente a sus elementos de manera secuencial. Imagine una estructura de datos auto empaquetada que descarga datos uno por uno en orden cuando se coloca dentro de un bucle for … of.
El concepto del protocolo iterable se puede dividir en iterable (la propia estructura de datos) y el iterador (una especie de puntero que se mueve sobre iterable). Consideremos un arreglo, por ejemplo, que se recorre en un bucle for … of, se llama a la propiedad iterable que devuelve un iterador. Esta propiedad iterable tiene un espacio de nombre como Symbol.iterator y el objeto que devuelve se puede usar en una interfaz común que comparten todas las estructuras de control de bucle.
En cierto modo, el Symbol.iterator se puede comparar con una fábrica de iteradores que produce un iterador cada vez que la estructura de datos se coloca en un bucle.
A medida que un iterador se desplaza sobre la estructura de datos y proporciona los elementos secuencialmente, el objeto devuelto por el iterable contiene un valor y una propiedad “terminada”.
El valor indica el valor de datos actual señalado por el iterador y “terminó” es un valor booleano que nos dice si el iterador ha alcanzado el último elemento en la estructura de datos.
Este {valor, terminó} es consumido por estructuras tales como bucles. Entonces, ¿cómo llama el método iterador al siguiente objeto? Usando un método next() que está definido dentro del método Symbol.iterator().
Una mejor definición de la propiedad del iterador que se puede encontrar en este punto es que es una propiedad que sabe cómo acceder a los elementos de una colección uno por uno y también proporciona una regla lógica para dejar de hacerlo cuando ya no hay mas elementos.
Objetos e iterables
Los objetos de JavaScript son geniales y todo, pero ¿por qué no tienen iterables? Bueno, algunas de las razones podrían ser:
- Una de las características clave de los objetos es que está definido por el usuario. Así que desplazarse en un [Symbol.iterator] () que no hace nada dentro del objeto sería una sorpresa desagradable.
- El punto anterior también significa que el usuario puede agregarlo manualmente, considerando que todas las composiciones de objetos podrían no ser similares. Por lo tanto, tener una propiedad común no tiene sentido.
Si quisiéramos que nuestro objeto personalizado tuviera su propio iterador, podríamos implementarlo de la siguiente forma:
let Comida = {
sabores: {
salada: ["carne", "mariscos"],
dulce: ["pasteles", "gelatina"]
},
[Symbol.iterator]() {
let comidaPorSabores = Object.values(this.sabores);
let indiceDeComida = 0;
let indiceDeSabores = 0;
return {
next() {
if (indiceDeComida >= comidaPorSabores[indiceDeSabores].length) {
indiceDeSabores++;
indiceDeComida = 0;
}
if (indiceDeSabores >= comidaPorSabores.length) {
return { value: undefined, done: true };
}
return {
value: comidaPorSabores[indiceDeSabores][indiceDeComida++],
done: false
};
}
};
}
};
// y ahora iteramos asi:
for (let sabor of Comida) console.log(sabor);
Lo que obtendríamos seria algo asi:
"carne"
"mariscos"
"pasteles"
"gelatina"
Con este ejemplo, vemos que los iteradores se pueden implementar dentro del objeto. Los iterables pueden ser propiedades poderosas para objetos que brindan facilidad de uso mientras se manejan ciertas situaciones y nos ayudan a evitar escribir nombres de ruta largos.
Obteniendo el iterador
Los bucles como for…of tienen un mecanismo incorporado para consumir iterables hasta que el valor de hecho se evalúe como verdadero. ¿Qué pasa si quieres consumir el iterable por tu cuenta, sin un bucle incorporado? Simple, obtienes el iterador de iterable y luego llamas a next () manualmente en él.
Dado el mismo ejemplo anterior, podríamos obtener un iterador de Comida llamando a su Symbol.iterator así:
let iteradorDeComida = Comida[Symbol.iterator]();
Y usar sus métodos internos así:
console.log(reptileIterator.next());
Y repetir hasta que el resultado sea “undefined” y el parámetro “done” sea true.