Los objetos iterables son una generalización de arrays. Es un concepto que permite que cualquier objeto pueda ser utilizado en un bucle for..of.
Por supuesto, las matrices o arrays son iterables. Pero hay muchos otros objetos integrados que también lo son. Por ejemplo, las cadenas o strings son iterables también. Como veremos, muchos operadores y métodos se basan en la iterabilidad.
Si un objeto no es técnicamente una matriz, pero representa una colección (lista, conjunto) de algo, entonces el uso de la sintaxis for..of es una gran forma de recorrerlo. Veamos cómo funciona.
Symbol.iterator
Podemos comprender fácilmente el concepto de iterables construyendo uno.
Por ejemplo: tenemos un objeto que no es un array, pero parece adecuado para for..of.
Como un objeto range que representa un intervalo de números:
let range = {
from: 1,
to: 5
};
// Queremos que el for..of funcione de la siguiente manera:
// for(let num of range) ... num=1,2,3,4,5
Para hacer que el objeto range sea iterable (y así permitir que for..of funcione) necesitamos agregarle un método llamado Symbol.iterator (un símbolo incorporado especial usado solo para realizar esa función).
- Cuando se inicia
for..of, éste llama al métodoSymbol.iteratoruna vez (o genera un error si no lo encuentra). El método debe devolver un iterador : un objeto con el métodonext(). - En adelante,
for..oftrabaja solamente con ese objeto devuelto. - Cuando
for..ofquiere el siguiente valor, llama anext()en ese objeto. - El resultado de
next()debe tener la forma{done: Boolean, value: any}, dondedone=truesignifica que el bucle ha finalizado; de lo contrario, el nuevo valor esvalue.
Aquí está la implementación completa de range:
let range = {
from: 1,
to: 5
};
// 1. Una llamada a for..of inicializa una llamada a esto:
range[Symbol.iterator] = function() {
// ... devuelve el objeto iterador:
// 2. En adelante, for..of trabaja solo con el objeto iterador debajo, pidiéndole los siguientes valores
return {
current: this.from,
last: this.to,
// 3. next() es llamado en cada iteración por el bucle for..of
next() {
// 4. debe devolver el valor como un objeto {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// ¡Ahora funciona!
for (let num of range) {
alert(num); // 1, luego 2, 3, 4, 5
}
Note una característica fundamental de los iterables: separación de conceptos.
- El
rangeen sí mismo no tiene el métodonext(). - En cambio, la llamada a
range[Symbol.iterator]()crea un otro objeto llamado “iterador”, y sunext()genera valores para la iteración.
Por lo tanto, el objeto iterador está separado del objeto sobre el que itera.
Técnicamente, podríamos fusionarlos y usar el range mismo como iterador para simplificar el código.
De esta manera:
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let num of range) {
alert(num); // 1, luego 2, 3, 4, 5
}
Ahora range[Symbol.iterator]() devuelve el objeto range en sí: tiene el método next()