Iterables
Los objetos iterables generalizan el concepto de arrays, permitiendo que cualquier objeto pueda utilizarse en un bucle for..of
. Además de los arrays, hay otros muchos objetos integrados que también son iterables. Por ejemplo, las cadenas de texto (strings) también lo son. Como veremos, muchos operadores y métodos dependen de la iterabilidad.
Aunque un objeto no sea técnicamente un array, si representa una colección (lista, conjunto, etc.) de elementos, es conveniente usar la sintaxis for..of
para recorrerlo. Veamos cómo funciona.
Symbol.iterator
Para comprender fácilmente el concepto de iterables, podemos crear uno. Supongamos que tenemos un objeto que no es un array, pero queremos que se pueda recorrer con for..of
. Por ejemplo, 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 que range
sea iterable, necesitamos agregarle un método llamado Symbol.iterator
(un símbolo incorporado especial utilizado para esa función).
Cuando se inicia for..of
, este llama al método Symbol.iterator
una vez (o genera un error si no lo encuentra). El método debe devolver un iterador: un objeto con el método next()
. A partir de ese momento, for..of
trabaja únicamente con ese objeto devuelto. Cuando for..of
necesita el siguiente valor, llama a next()
en ese objeto. El resultado de next()
debe tener la forma {done: Boolean, value: any}
, donde done=true
indica que el bucle ha finalizado; de lo contrario, el nuevo valor es value
.
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
}
Notemos una característica esencial de los iterables: la separación de conceptos. El objeto range
en sí mismo no tiene el método next()
. En su lugar, la llamada a range[Symbol.iterator]()
crea otro objeto llamado “iterador”, y su next()
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()
necesario y recuerda el progreso de iteración actual en this.current
. Es más corto y, a veces, eso también está bien.
La desventaja es que ahora es imposible tener dos bucles for..of
corriendo sobre el objeto simultáneamente: compartirán el estado de iteración, porque solo hay un iterador: el objeto en sí. Pero dos for-of
paralelos son raros, incluso en escenarios asíncronos.
Iteradores Infinitos
También es posible tener iteradores infinitos. Por ejemplo, el objeto range
puede volverse infinito si range.to = Infinity
. O podemos crear un objeto iterable que genere una secuencia infinita de números pseudoaleatorios. Esto puede ser útil en algunos casos.
Por supuesto, el bucle for..of
sobre un iterable de este tipo sería interminable. Pero siempre podemos detenerlo usando break
.
String es iterable
Las matrices y cadenas son los iterables integrados más utilizados.
En una cadena o string, el bucle for..of
recorre sus caracteres:
for (let char of "test") {
alert(char); // t, luego e, luego s, luego t
}
¡Y funciona correctamente con valores de pares sustitutos (codificación UTF-16)!
let str = '𝒳😂';
for (let char of str) {
alert(char); // 𝒳, y luego 😂
}
Llamar a un iterador explícitamente
Para una comprensión más profunda, veamos cómo usar un iterador explícitamente.
Vamos a iterar sobre una cadena exactamente de la misma manera que for..of
, pero con llamadas directas. Este código crea un iterador de cadena y obtiene valores de él “manualmente”:
let str = "Hola";
// hace lo mismo que
// for (let char of str) alert(char);
let iterator = str[Symbol.iterator]();
while (true) {
let result = iterator.next();
if (result.done) break;
alert(result.value); // retorna los caracteres uno por uno
}
Rara vez se necesita esto, pero nos da más control sobre el proceso que for..of
. Por ejemplo, podemos dividir el proceso de iteración: iterar un poco, luego parar, hacer otra cosa y luego continuar.
Iterables y simil-array (array-like)
Los dos son términos oficiales que se parecen, pero son muy diferentes. Asegúrese de comprenderlos bien para evitar confusiones.
- Los iterables son objetos que implementan el método
Symbol.iterator
, como se describió anteriormente. - Los objetos «simil-array» tienen índices y longitud (
length
), por lo que se parecen a arrays.
Cuando usamos JavaScript para tareas prácticas en el navegador u otros entornos, podemos encontrar objetos que son iterables o «simil-array», o ambos.
Por ejemplo, las cadenas son iterables (se pueden recorrer con for..of
) y también son «simil-array» (tienen índices numéricos y longitud).
Pero un iterable puede que no sea «simil-array». Y viceversa, un «simil-array» puede no ser iterable.
Por ejemplo, range
en el ejemplo anterior es iterable, pero no es «simil-array» porque no tiene propiedades indexadas ni longitud.
Y aquí hay un objeto que tiene forma de matriz, pero no es iterable:
let arrayLike = {
0: "Hola",
1: "Mundo",
length: 2
};
// Error (sin Symbol.iterator)
for (let item of arrayLike) {}
Tanto los iterables como los «simil-array» generalmente no son arrays reales, no tienen métodos como push
, pop
, etc. Eso puede ser incómodo si queremos trabajar con ellos como si fueran arrays. ¿Cómo logramos esto?
Array.from
Existe un método universal Array.from
que toma un valor iterable o «simil-array» y crea un array real a partir de él. De esta manera, podemos usar métodos que pertenecen a un array.
Por ejemplo:
let arrayLike = {
0: "Hola",
1: "Mundo",
length: 2
};
let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // Mundo (el método pop funciona)
Array.from
en la línea (*) toma el objeto, y si es iterable o «simil-array» crea un nuevo array y copia allí todos los elementos.
Lo mismo sucede para un iterable:
// suponiendo que range se toma del ejemplo anterior
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (la conversión de matriz a cadena funciona)
La sintaxis completa para Array.from
también nos permite proporcionar una función opcional de “mapeo”:
Array.from(obj[, mapFn, thisArg])
El segundo argumento opcional mapFn
puede ser una función que se aplicará a cada elemento antes de agregarlo a la matriz, y thisArg
permite establecer el this
para ello.
Por ejemplo:
// suponiendo que range se toma del ejemplo anterior
// el cuadrado de cada número
let arr = Array.from(range, num => num * num);
alert(arr); // 1,4,9,16,25
Aquí usamos Array.from
para convertir una cadena en una matriz de caracteres:
let str = '𝒳😂';
// separa str en un array de caracteres
let chars = Array.from(str);
alert(chars[0]); // 𝒳
alert(chars[1]); // 😂
alert(chars.length); // 2
A diferencia de str.split
, Array.from
se basa en la naturaleza iterable de la cadena y, por lo tanto, al igual que for..of
, funciona correctamente con pares sustitutos.
Técnicamente hace lo mismo que:
let str = '𝒳😂';
let chars = []; // Array.from internamente hace el mismo bucle
for (let char of str) {
chars.push(char);
}
alert(chars);
… Pero es más corto.
Incluso podemos construir un segmento compatible con sustitutos en él:
function slice(str, start, end) {
return Array.from(str).slice(start, end).join('');
}
let str = '𝒳😂𩷶';
alert(slice(str, 1, 3)); // 😂𩷶
// el método nativo no admite pares sustitutos
alert(str.slice(1, 3)); // basura (dos piezas de diferentes pares sustitutos)
Resumen
- Los objetos que se pueden usar en
for..of
se denominan iterables. - Técnicamente, los iterables deben implementar el método llamado
Symbol.iterator
. - El resultado de
obj[Symbol.iterator]()
se llama iterador. Maneja el proceso de iteración adicional. - Un iterador debe tener el método llamado
next()
que devuelve un objeto{done: Boolean, value: any}
, dondedone: true
marca el fin de la iteración; de lo contrario,value
es el siguiente valor. - El método
Symbol.iterator
se llama automáticamente porfor..of
, pero también podemos llamarlo directamente. - Los iterables integrados, como cadenas o matrices, también implementan
Symbol.iterator
. - El iterador de cadena es capaz de manejar los pares sustitutos.
- Los objetos que tienen propiedades indexadas y longitud (
length
) se llaman «simil-array». Dichos objetos también pueden tener otras propiedades y métodos, pero carecen de los métodos integrados de las matrices.
En la especificación, la mayoría de los métodos incorporados suponen que funcionan con iterables o «simil-array» en lugar de matrices reales, porque eso es más abstracto.
Array.from(obj[, mapFn, thisArg])
crea un verdadero array a partir de un objeto iterable o «simil-array», permitiendo así el uso de métodos de matriz en él. Los argumentos opcionales mapFn
y thisArg
nos permiten aplicar una función a cada elemento antes de agregarlo a la matriz.