¿Qué son los iteradores?

Estructuras iterables o no iterables


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 y lo utilizaremos como el índice o posición en el 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 a data[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 Un que 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 , podemos obtener todos los valores y construir un array con todos ellos:

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 esta iteración automática no tiene sentido, porque es el propio original, pero nos sirve para ejemplificar un caso muy fácil, para ver el siguiente ejemplo donde si que es necesario.

Estructuras no iterables

No todas las estructuras de datos son iterables por defecto. Por ejemplo, un es iterable de serie, pero un no lo es. Sin embargo, ahora que conocemos los iteradores explícitos, podemos crear un iterador personalizado para estructuras que en principio no son iterables.

Veamos algunos ejemplos.

Sin iteradores

En este caso, hemos creado un que no tiene ningún iterador definido. Simplemente, creamos un objeto con dos propiedades: 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 , nos devolverá un error como el siguiente:

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 y end 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 con value y done.
  • 4️⃣ En nuestro caso, devolvemos el valor de value incrementado y done a falso.
  • 5️⃣ Si value llega a end, entonces devolvemos done a true.
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.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev