Los objetos iterables son una generalización de matrices. Ese es un concepto que nos permite hacer que cualquier objeto sea utilizable en un bucle for..of
.
Por supuesto, las matrices son iterables. Pero hay muchos otros objetos integrados que también son iterables. Por ejemplo, las cadenas también son iterables.
Si un objeto no es técnicamente una matriz, pero representa una colección (lista, conjunto) de algo, entonces for..of
es una excelente sintaxis para recorrerlo, así que veamos cómo hacerlo funcionar.
Podemos comprender fácilmente el concepto de iterables creando uno propio.
Por ejemplo, tenemos un objeto que no es una matriz, pero parece adecuado para for..of
.
Como un objeto range
que representa un intervalo de números:
dejar rango = { de: 1, a: 5 }; // Queremos que el for..of funcione: // for(let num de rango) ... num=1,2,3,4,5
Para hacer que el objeto range
sea iterable (y así permitir for..of
trabajo), necesitamos agregar un método al objeto llamado Symbol.iterator
(un símbolo incorporado especial solo para eso).
Cuando se inicia for..of
, llama a ese método una vez (o genera errores si no se encuentra). El método debe devolver un iterador : un objeto con el método next
.
En adelante, for..of
funciona solo con ese objeto devuelto .
Cuando for..of
quiere el siguiente valor, llama next()
en ese objeto.
El resultado de next()
debe tener la forma {done: Boolean, value: any}
, donde done=true
significa que el ciclo ha finalizado; de lo contrario, value
es el siguiente valor.
Aquí está la implementación completa para range
con comentarios:
dejar rango = { de: 1, a: 5 }; // 1. llama a for..of inicialmente llama a esto rango[Símbolo.iterador] = función() { // ...devuelve el objeto iterador: // 2. En adelante, for..of funciona solo con el objeto iterador siguiente y le solicita los siguientes valores devolver { actual: this.from, último: this.to, // 3. next() es llamado en cada iteración por el bucle for..of próximo() { // 4. debería devolver el valor como un objeto {hecho:.., valor:...} if (este.actual <= este.último) { return {hecho: falso, valor: this.current++}; } demás { devolver {hecho: verdadero}; } } }; }; // ¡ahora funciona! for (sea el número de rango) { alerta(núm); // 1, luego 2, 3, 4, 5 }
Tenga en cuenta la característica principal de los iterables: la separación de preocupaciones.
El range
en sí no tiene el método next()
.
En cambio, la llamada a range[Symbol.iterator]()
crea otro objeto, el llamado "iterador", y su next()
genera valores para la iteración.
Entonces, el objeto iterador está separado del objeto sobre el que itera.
Técnicamente, podemos fusionarlos y usar el propio range
como iterador para simplificar el código.
Como esto:
dejar rango = { de: 1, a: 5, [Símbolo.iterador]() { esto.actual = esto.de; devolver esto; }, próximo() { si (este.actual <= este.a) { return {hecho: falso, valor: this.current++}; } demás { devolver {hecho: verdadero}; } } }; for (sea el número de rango) { alerta(núm); // 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 la iteración actual en this.current
. ¿Más corto? Sí. Y a veces eso también está bien.
La desventaja es que ahora es imposible tener dos bucles for..of
ejecutándose sobre el objeto simultáneamente: compartirán el estado de iteración, porque solo hay un iterador: el objeto mismo. Pero dos for-ofs paralelos son algo raro, incluso en escenarios asíncronos.
iteradores infinitos
También son posibles iteradores infinitos. Por ejemplo, el range
se vuelve infinito para range.to = Infinity
. O podemos crear un objeto iterable que genere una secuencia infinita de números pseudoaleatorios. También puede resultar útil.
No hay limitaciones en next
, puede devolver más y más valores, eso es normal.
Por supuesto, el bucle for..of
sobre dicho iterable sería interminable. Pero siempre podemos detenerlo usando break
.
Las matrices y las cadenas son los iterables integrados más utilizados.
Para una cadena, for..of
recorre sus caracteres:
for (let char de "prueba") { // se activa 4 veces: una por cada personaje alerta( carbón ); // t, luego e, luego s, luego t }
¡Y funciona correctamente con parejas de alquiler!
let str = '??'; para (dejar char de str) { alerta( carbón ); // ?, y luego ? }
Para una comprensión más profunda, veamos cómo usar un iterador explícitamente.
Iteraremos 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 iterador = str[Símbolo.iterador](); mientras (verdadero) { let resultado = iterador.next(); si (resultado.hecho) se rompe; alerta(resultado.valor); // genera caracteres uno por uno }
Esto rara vez es necesario, 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 detenerlo, hacer otra cosa y luego reanudarlo más tarde.
Dos términos oficiales parecen similares, 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 describe anteriormente.
Los tipos de matrices son objetos que tienen índices y length
, por lo que parecen matrices.
Cuando utilizamos JavaScript para tareas prácticas en un navegador o cualquier otro entorno, podemos encontrar objetos que son iterables o similares a matrices, o ambos.
Por ejemplo, las cadenas son iterables ( for..of
trabajos en ellas) y similares a matrices (tienen índices numéricos y length
).
Pero un iterable puede no ser similar a una matriz. Y viceversa, una matriz puede no ser iterable.
Por ejemplo, el range
en el ejemplo anterior es iterable, pero no similar a una matriz, porque no tiene propiedades ni length
indexadas.
Y aquí está el objeto que es similar a una matriz, pero no iterable:
let arrayLike = { // tiene índices y longitud => similar a una matriz 0: "Hola", 1: "Mundo", longitud: 2 }; // Error (sin símbolo.iterador) for (dejar elemento de arrayLike) {}
Tanto los iterables como los similares a matrices generalmente no son matrices , no tienen push
, pop
, etc. Esto es bastante inconveniente si tenemos un objeto de este tipo y queremos trabajar con él como si fuera una matriz. Por ejemplo, nos gustaría trabajar con range
usando métodos de matriz. ¿Cómo lograr eso?
Existe un método universal Array.from que toma un valor iterable o similar a una matriz y crea una Array
"real" a partir de él. Luego podemos llamar a métodos de matriz.
Por ejemplo:
let arrayComo = { 0: "Hola", 1: "Mundo", longitud: 2 }; let arr = Array.from(arrayLike); // (*) alerta(arr.pop()); // Mundo (el método funciona)
Array.from
en la línea (*)
toma el objeto, lo examina para ver si es iterable o similar a una matriz, luego crea una nueva matriz y copia todos los elementos en ella.
Lo mismo ocurre con un iterable:
// suponiendo que el rango se haya tomado del ejemplo anterior let arr = Array.from(rango); alerta(arr); // 1,2,3,4,5 (la conversión de matriz a cadena funciona)
La sintaxis completa de Array.from
también nos permite proporcionar una función de "mapeo" opcional:
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
nos permite this
.
Por ejemplo:
// suponiendo que el rango se haya tomado del ejemplo anterior // eleva al cuadrado cada número let arr = Array.from(rango, num => num * num); alerta(arr); // 1,4,9,16,25
Aquí usamos Array.from
para convertir una cadena en una matriz de caracteres:
let str = '??'; // divide str en una serie de caracteres let caracteres = Array.from(str); alerta(caracteres[0]); // ? alerta(caracteres[1]); // ? alerta(caracteres.longitud); // 2
A diferencia de str.split
, 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 aquí hace lo mismo que:
let str = '??'; dejar caracteres = []; // Array.from internamente hace el mismo ciclo para (dejar char de str) { caracteres.push(char); } alerta(caracteres);
…Pero es más corto.
Incluso podemos crear slice
compatible con sustitutos:
función segmento(cadena, inicio, fin) { return Array.from(str).slice(inicio, fin).join(''); } let str = '???'; alerta( segmento(cadena, 1, 3) ); // ?? // el método nativo no soporta pares sustitutos alerta( str.slice(1, 3) ); // basura (dos piezas de diferentes pares sustitutos)
Los objetos que se pueden utilizar en for..of
se denominan iterables .
Técnicamente, los iterables deben implementar el método denominado Symbol.iterator
.
El resultado de obj[Symbol.iterator]()
se llama iterador . Maneja un proceso de iteración adicional.
Un iterador debe tener el método llamado next()
que devuelve un objeto {done: Boolean, value: any}
, aquí done:true
denota el final del proceso de iteración; de lo contrario, el value
es el siguiente valor.
El método Symbol.iterator
es llamado automáticamente por for..of
, pero también podemos hacerlo directamente.
Los iterables integrados, como cadenas o matrices, también implementan Symbol.iterator
.
El iterador de cadenas conoce los pares sustitutos.
Los objetos que tienen propiedades y length
indexadas se denominan tipo matriz . Estos objetos también pueden tener otras propiedades y métodos, pero carecen de los métodos integrados de las matrices.
Si miramos dentro de la especificación, veremos que la mayoría de los métodos integrados asumen que funcionan con iterables o similares a matrices en lugar de matrices "reales", porque eso es más abstracto.
Array.from(obj[, mapFn, thisArg])
crea un Array
real a partir de un obj
iterable o similar a un array, y luego podemos usar métodos de array en él. Los argumentos opcionales mapFn
y thisArg
nos permiten aplicar una función a cada elemento.