preloader

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}, donde done: 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 por for..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.

Related Post

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *