Existe un viejo problema en la informática que consiste en recorrer objetos de forma ordenada. La solución de Javascript ES6 a este problema son los iteradores que, como veremos, tienen una sintaxis algo compleja. En este post, vamos a revisar esta sintaxis de iteradores en ES6 y mostraré una alternativa más simple mediante el uso de los generadores.

Iteradores en ES6

Un iterador es un objeto que implementa el interfaz Iterator. Este interfaz es simplemente un objeto que tiene una clave next, la cual es una función que devuelve un objeto con dos claves: value y done. Parece más complejo de lo que es. Tal vez este ejemplo ayude a explicarlo mejor:

 let myObjectIterable = {
  "keyWhichImplementIteratorInterface": function(){
      return {

        next(){
          // Debo devolver un objeto con dos claves `value` y `done`
        }

    }
  }
 }

"myObjectIterable" es iterable gracias a que contiene la clave keyWhichImplementIteratorInterface, la cual devuelve el interfaz Iterator.

Como no es muy práctico usar nombres de claves como keyWhichImplementIteratorInterface, en ES6 usamos los símbolos para designar qué clave del objeto es la encargada de devolvernos el interfaz Iterator. En este caso el símbolo es Symbol.iterator. Este símbolo actuará como marca dentro de nuestro objeto y todo aquel que quiera iterar a través de él sabrá dónde preguntar por el interfaz iterator.

Veamos un ejemplo en el siguiente código:

let count = 0;

let myObject = {  
  [Symbol.iterator]() {
    return {
      next(){
        if( count++ < 5 ){
          return { value: count, done:false }
        }
        else{
          return {done: true}
        }
      }
    }
  }
};

console.log( "Primero recorremos el objeto con un 'for of' bucle" )  
for( let value of myObject ){  
  console.log( value )
}

console.log( "Ahora lo vamos a recorrer llamando nosotros el método next a mano 5 veces" )  
count = 0;  
let myObjectIterator = myObject[Symbol.iterator]();  
let primeraLlamada = myObjectIterator.next();  
console.log( "Value: %d // done: %s", primeraLlamada.value, primeraLlamada.done )

let segundaLlamada = myObjectIterator.next();  
console.log( "Value: %d // done: %s", segundaLlamada.value, segundaLlamada.done )

let terceraLlamada = myObjectIterator.next();  
console.log( "Value: %d // done: %s", terceraLlamada.value, terceraLlamada.done )

let cuartaLlamada = myObjectIterator.next();  
console.log( "Value: %d // done: %s", cuartaLlamada.value, cuartaLlamada.done )

let quintaLlamada = myObjectIterator.next();  
console.log( "Value: %d // done: %s", quintaLlamada.value, quintaLlamada.done )

let sextaLlamada = myObjectIterator.next();  
console.log( "Value: %d // done: %s", sextaLlamada.value, sextaLlamada.done )  

Usamos for of cuando queremos iterar a través de un objeto que sabemos que tiene una clave que devuelve un iterador.

En el ejemplo vemos cómo funciona la iteración, tanto con el bucle for-of, como llamando nosotros a mano al método next hasta que nos devuelve {done: true}. La salida sería algo así:

Y eso es un iterador. Una función next que llamamos tantas veces como sea necesario hasta obtener {done:true}.

La iteración no solo se aplica a objetos: podemos hacer lo mismo con una clase, siguiendo un razonamiento similar. Tenemos que definir una método de clase, cuyo nombre sea Symbol.iterator y que devuelva el interfaz iterator.

let MyClass = class MyClass {  
  constructor( sequence = [] ){
    this._sequence = sequence;
    this._index = 0;
  }

  push( number ){
    this._sequence.push( number );
  }

  [Symbol.iterator]() {
    let self = this;
    return {
      next() {
        if( self._index < self._sequence.length ){
          return { value: self._sequence[self._index++], done: false };
        } else {
          return { done: true }
        }
      }
    }
  }
}

let myInstance = new MyClass( [1,2,3,4] );  
myInstance.push( 5 );  
myInstance.push( 6 );

for( let value of myInstance ){  
  console.log( value );
}

Esta sería la salida:

Muchas de las clases de JavaScript ya son iterables. Entre las detacadas tenemos Arrays, String, Set, Map, NodeList y seguramente algunas más que ahora mismo no recuerdo. Se comportan de la misma manera que los objetos iterables:

let array = [1, 2, 3, 4]  
let string = "Hola mundo!"  
let set = new Set( ['set1', 'set2', 'set3'] )  
let map = new Map();  
map.set( 'key one', 'value one' )  
map.set( 'key two', 'value two' )  
map.set( 'key three', 'value three' )


// Iterando un array
for( let value of array ){  
  console.log( `Array value ${value}` );
}

// Iterando strings
for( let character of string ){  
  console.log( `Character ${character}` )
}

// Iterando un Set
for( let item of set ){  
  console.log( `Item in set ${item}` )
}

// Iteramos por los valores de un map
for( let value of map.values()  ){  
  console.log( `Value: ${value}` )
}

// Iteramos por las claves de un map
for( let key of map.keys()  ){  
  console.log( `Key: ${key}` )
}

// Iteramos por las claves y los valores de un map
for( let [key, value] of map.entries()  ){  
  console.log( `Key: ${key} => Value: ${value}` )
}

Esta es la salida que obtenemos:

No quiero entrar en los detalles de cada una de esas nuevas clases, pero si tienes curiosidad por saber más de ellas, Mozilla Developer Network es un lugar idóneo para empezar.

Es evidente que la sintaxis de los iteradores, aunque simple, es bastante engorrosa. A continuación veremos una simplificación de esa sintaxis mediante el concepto de generadores.

Generadores en ES6

Los generadores son un tipo especial de función que devuelve un valor y te permite luego volver a entrar en la función en el mismo lugar en que la dejaste, al tiempo que conserva el contexto de ejecución. Por ejemplo:

let generatorArray = function* ( array ){  
  for( let i=0; i< array.length; i++ ){
    yield array[i]
  }
};

let iterator = generatorArray( [1, 2 ,3, 4] )  
for( let item of iterator ){  
  console.log( `Este es el item: ${item}` )
}

Cuando se llama a los los generadores, implícitamente devuelven un iterador; este, a su vez, devuelve el valor asociado a cada iteración mediante el uso de yield.

El secreto de la elegancia del generador radica, pues, en la palabra clave yield. yield es un tipo especial de returnque, en lugar de devolver un solo valor y salirse de la función, entrará nuevamente en esta y continuará ejecutándola hasta que se acabe o encuentre otra cláusula yield.

Visto así la salida de esa función sería:

Fíjate cómo salimos de la función en cada iteración del bucle, devolviendo el valor del array para el índice en ese momento, y luego volvemos a entrar en la función y ejecutamos la siguiente iteración del bucle. Indudablemente, esta sintaxis es muchísimo más clara que tener que definir nosotros a mano el interfaz Iterator.

Los ejemplos usados aquí los puedes encontar en Github

Si te ha gustado este post, difunde la palabra. Tampoco dudes en dejar comentarios u observaciones. ¡Gracias! :)

Suscríbete a mi lista de correo

* Campos obligatorios