¡Hola! Me parece que andas usando un bloqueador de anuncios =( ¿Nos desbloqueas? ¿Por qué?
F13

Funcion 13

Aprende ES6 - Iteradores e Iterables

Como ya sabrás, en Función 13 estamos embarcados en que aprendas todos los entresijos de ES6. En artículos anteriores hemos hablado sobre let, const y los ámbitos, las funciones flecha, que son realmente útiles, las Promesas y las Clases, cómo podemos usar la Desestructuración o sobre parámetros y propagación. Además hemos aprendido qué nuevas opciones tienen los objetos literales, las matrices (o arrays) y las cadenas. Si esto no te suena de nada y estás intentando ponerle algún sentido a las siglas, ¡no te preocupes! Echa un vistazo a nuestra Introducción a ES6.

ES6 nos trae el concepto nuevo de Iterators o Iteradores aunque sin duda no es un concepto nuevo en otros lenguajes como en Java. Vamos a explicar qué son, qué cosas son iterables por defecto, cómo podemos crear nuestros propios objetos iterables y cómo podemos consumirlos. Aviso de que este tema es un poco más denso de lo que hemos visto hasta ahora.

El concepto de Iterable podríamos definirlo como una Interfaz.

¿Y qué leches es una interfaz?

Bueno, una interfaz no deja de ser una serie de instrucciones para implementar algo de manera que al hacerlo, se cumplan una serie de expectativas. Las expectativas nos dan tranquilidad y nos permiten saber, de antemano, cómo deberíamos usar algo. Por ejemplo, sabemos que un coche suele tener una (o más) puertas por las que entrar, un volante para maniobrar y un dispositivo de arranque (llave o botón).

Pues bien, los iterables definen que:

  • Se debe de poder iterar solo por un elemento cada vez.
  • Se debe poder saber cuándo se ha acabado de recorrer.

En la cadena de los iterables tenemos, de un lado, los que consumen estos datos. Es decir, ellos esperan que se cumplan las normas citadas para saber cómo poder recorrer el iterable. Por ejemplo tenemos el nuevo bucle for-of y el parámetro de propagación ... del que hablamos recientemente.

Del otro lado tenemos las fuentes de datos que han de implementar la interfaz de iterable como las matrices (Array), los Mapas (de los que ya hablaremos), etc.

Los consumidores usan la interfaz y las fuentes la implementan:

No obstante, al contrario que otros lenguajes, JavaScript no tiene interfaces por lo que el concepto de Iterable es una convención. Lamento si te decepcioné:

  • Fuentes de datos: Un valor es considerado iterable si tiene un método cuya clave es Symbol.iterator. El iterador es un objeto que devuelve valores a través del método next(). Como dijimos antes, enumera los elementos, uno por cada llamada al método next().
  • Consumidores: Los consumidores usan el iterador para obtener los valores.

No te preocupes si el tema de Symbol.iterator no tiene mucho sentido ahora mismo. Vamos a ver cómo funciona todo esto para una matriz.

let matriz = ['foo', 'bar', 'baz'];  
let iterador = matriz[Symbol.iterator];  

Como ves, creamos un iterador a través del método cuya clave es Symbol.iterator. Una vez hecho esto, podemos usar el método next para ver lo que nos devuelve:

console.log(iterador.next());  
// {value: 'foo', done: false}
console.log(iterador.next());  
// {value: 'bar', done: false}
console.log(iterador.next());  
// {value: 'baz', done: false}
console.log(iterador.next());  
// {value: undefined, done: true}

El método next() (siguiente en la lengua de Cervantes) devuelve cada elemento envuelto en un objeto con el valor en la propiedad value y una propiedad done que indica si se ha llegado (o no) al final.

Fuentes de datos iterables

Vamos a usar el bucle for-of por ahora, para ir viendo poco a poco cómo iteramos sobre cada elemento. Como verás, no es necesario crear un iterador.

Matrices

for (let x of ['funcion', '13']) {  
    console.log(x);
}
// 'funcion'
// '13'

Cadenas

let string = 'foo'

for (let x of string) {  
  console.log(x);
}

// 'f'
// 'o'
// 'o'

Como ves, los valores primitivos también pueden ser iterables, no solo los objetos.

Argumentos

Aunque la variable especial arguments queda un poco obsoleta en ES6 gracias al resto de parámetros, es iterable:

function mostrarArgumentos() {  
  for (let x of arguments) {
    console.log(x);
  }
}

mostrarArgumentos('foo', 'bar');  
// 'foo'
// 'bar'

Hay más iterables pero aun no hemos hablado sobre ellos así que no vamos a liarnos más.

Iterando iterables

Veamos cómo podemos consumir los iterables que hemos mostrado.

Desestructurando

Es posible desestructurar cualquier iterable:

let lista = ['a', 'b', 'c'];  
let cadena = 'foo'  
let [x, y] = lista;  
let [a, b, c] = cadena;

console.log(x); // 'a'  
console.log(y); // 'b'

console.log(a); // 'f'  
console.log(b); // 'o'  
console.log(c); // 'o'  

El bucle for-of

El bucle for-of es el que hemos estado viendo hasta ahora. Veamos su forma más básica:

for (let x of iterable) {  
    ···
}

Este bucle itera sobre la variable iterable y asigna cada uno de los elementos enumerados a la variable x y te permite procesarla en el cuerpo del bucle, debido que en este caso, la variable ha sido asignada con let, x solo es accesible dentro del propio bucle.

Ten en cuenta que es totalmente necesario que un elemento implemente el concepto iterable de lo contrario el bucle no podrá funcionar correctamente. Puedes valerte de métodos como Array.from para convertir valores que se parezcan a matrices, en matrices (que son iterables).

Por otro lado, es totalmente posible no declarar la variable y hacerlo fuera del propio bucle o usar cualquier otra forma. Veamos algunos ejemplos:

let x;  
let matriz = ['funcion', '13'];  
let obj = {};  
let otraMatriz = [];

// Variable declarada fuera
for (x of matriz) {  
  console.log(x);
}

// Variable sobre propiedad de objeto
for (obj.prop of matriz) {  
  console.log(obj.prop);
}

// Variable sobre elemento de matriz
for (otraMatriz[0] of matriz) {  
  console.log(otraMatriz[0]);
}

Array.from

Aunque ya lo comentamos cuando hablamos de las matrices, Array.from nos permite convertir un objeto iterable, a una matriz (que no deja de ser otro objeto iterable como ya hemos mencionado).

Definiendo nuestro propio iterable

Aunque ahora mismo no se me ocurre ningún caso de uso en concreto, es posible que en algún momento necesitemos crear un objeto que cumpla con este contrato de iterabilidad. Ya tenemos todas las herramientas del puzzle para saber cómo hacerlo. Vamos a ponerlas todas juntas a ver qué sacamos.

Para crear nuestro propio iterable, tenemos que crear una propiedad en el objeto con la clave Symbol.iterator la cual sea una función y devuelva como resultado al ser invocada un objeto con una función en la propiedad next que devuelva otro objeto con las propiedades value (indicando la actual propiedad siendo iterada) y done que indica si se ha completado o no todo el conjunto.

Veamos un ejemplo para ver todo esto más claro:

class MiIterable {  
  constructor() {
    this._frutas = [
      'plátanos',
      'manzanas',
      'sandías',
      'peras',
      'melocotones'
    ];
  }

  [Symbol.iterator]() {
    let indice = -1;
    let frutas  = this._frutas;

    return {
      next: () => ({ 
        value: frutas[++indice], 
        done: !(indice in frutas) 
      })
    };
  };
}

Aquí todo empieza ya a tener sentido. Espero.

Nuestra clase tiene una propiedad _frutas, en la que guardamos un listado de... frutas. Creamos una función en la propiedad Symbol.iterator. La función actúa como un closure cuando se comienza la iteración por lo que nos sirve para inicializar las variables indice y frutas. Como ves, devolvemos un objeto con una sola propiedad: next. En este caso usamos una función flecha (ya que devolvemos un objeto directamente) y devolvemos un objeto con propiedades value y done. Conteniendo el valor de cada fruta en cada iteración y si hemos acabado, ya que una vez que indice tenga un valor fuera del rango de la matriz la expresión indice in frutas devolverá true.

Ahora vamos a consumirlo para ver si funciona todo esto:

let iterable = new MiIterable();

for (let fruta of iterable) {  
  console.log(fruta)
  // <- 'plátanos'
  // <- 'manzanas'
  // <- 'sandías'
  // <- 'peras'
  // <- 'melocotones'
}

console.log([...iterable]);  
// ["plátanos", "manzanas", "sandías", "peras", "melocotones"]

console.log(Array.from(iterable));  
// ["plátanos", "manzanas", "sandías", "peras", "melocotones"]

¡Genial!

Aunque recordemos que las matrices son iterables directamente por lo que esto también serviría:

¡Hasta aquí hoy! ¿Dudas? En el próximo episodio hablaremos sobre los generadores.

Programador Front-end en First + Third y Potato. Trabajando con JavaScript y HTML5 desde el corazón de Sevilla.

Comentarios ¡Únete a la conversación!