Iteradores#

Iterables en JavaScript#

En JavaScript, los iterables son estructuras de datos que permiten iterar sobre sus elementos de manera secuencial. Los iterables más comunes son: Array, Set, Map y String. Los objetos normales no son iterables por defecto. Para que un objeto sea iterable, debe tener una propiedad Symbol.iterator que devuelva un iterador.

El iterador más común es de los Arrays, pero no todos los iteradores se pueden expresar o convertir en Arrays. Hay iteradores que son consumidos si es necesario y pueden tener longitud infinita.

Propiedad Symbol.iterator#

La propiedad Symbol.iterator es una función que retorna un objeto con el método next(). Este método devuelve un objeto con dos propiedades: value y done. value contiene el siguiente valor en la secuencia y done indica si la iteración ha terminado.

let someString = "Hello";
let iterator = someString[Symbol.iterator]();

console.log(iterator.next()); // { value: 'H', done: false }
console.log(iterator.next()); // { value: 'e', done: false }
console.log(iterator.next()); // { value: 'l', done: false }
{ value: "H", done: false }
{ value: "e", done: false }
{ value: "l", done: false }

Si simplemente hacemos un objeto que tenga una propiedad next() que retorne {value, done}, estamos haciendo un iterador, pero tenemos que usar explícitamente su método next(). En cambio, si el método está en Symbol.iterator, se pueden usar for..of o el spread operator.

Función generadora#

Una función generadora puede ser usada para crear iterables. Una función generadora se define con la sintaxis function*. Esta retorna un objeto de tipo Generator, que ya es un iterable. Los Generadores usan la expresión yield. Esta para la ejecución y retorna el valor. La siguiente vez que se invoca next(), ejecutará hasta el siguiente yield o return, que finaliza enviando done: true.

Documentación: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*

let myIterable = {};
myIterable[Symbol.iterator] = function* () {
   yield 1;
   yield 2;
   yield 3;
};
console.log([...myIterable]); // [1, 2, 3]
[ 1, 2, 3 ]
// Ejemplo sacado de: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_generators#generator_functions
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
    let iterationCount = 0;
    for (let i = start; i < end; i += step) {
      iterationCount++;
      yield i;
    }
    return iterationCount;
  }

  let generator = makeRangeIterator(0,10,1);
  console.log(generator.next(),generator.next());
  console.log([...generator]);
  
{ value: 0, done: false } { value: 1, done: false }
[
  2, 3, 4, 5,
  6, 7, 8, 9
]

Recorrer iterables#

Hay muchas maneras de recorrer un iterable. También hay muchos tipos de iterables y algunos tienen sus propias maneras de ser recorridos. En todo caso, un iterable debería poder convertirse en un Array y poderse recorrer de la manera estándar.

El método querySelector retorna un NodeList, que es un objeto que contiene un método forEach. Este es un ejemplo de iterable que no es un Array. En este caso el forEach no es el mismo que el de los Arrays y puede inducir a confusión.

Algunos ejemplos:

let a = [1, 2, 3, 4, 5];
a.forEach(element => { console.log(element); }); // 1, 2, 3, 4, 5

let b = a.map(function(item) { return item ** 2; });
console.log(b); // [1, 4, 9, 16, 25]

console.log(a.filter(function(item) { return item % 2 == 0; })); // [2, 4]

let total = a.reduce(function(previous, current) {
  console.log(previous, current);
  return previous + current;
}, 0);
console.log(total); // 15

let iterable = [10, 20, 30];
for (let value of iterable) {
  value += 1;
  console.log(value); // 11, 21, 31
}

function f(x, y, z) { }
let args = [0, 1, 2];
f(...args); // Llama a la función f con los argumentos 0, 1 y 2

let parts = ['shoulder', 'knees'];
let lyrics = ['head', ...parts, 'and', 'toes'];
console.log(lyrics); // ["head", "shoulder", "knees", "and", "toes"]
1
2
3
4
5
[ 1, 4, 9, 16, 25 ]
[ 2, 4 ]
0 1
1 2
3 3
6 4
10 5
15
11
21
31
[ "head", "shoulder", "knees", "and", "toes" ]

Métodos Iterativos de los Arrays#

Los arrays en JavaScript tienen varios métodos iterativos que toman una función de callback como argumento. Esta función de callback se llama secuencialmente y como máximo una vez por cada elemento del array. El valor de retorno de la función de callback determina el valor de retorno del método iterativo. Todos estos métodos comparten la misma firma:

método(callbackFn, thisArg)

Donde callbackFn toma tres argumentos:

  1. elemento: El elemento actual que se está procesando en el array.

  2. índice: El índice del elemento actual que se está procesando en el array.

  3. array: El array sobre el cual se llamó el método.

Lo que se espera que callbackFn retorne depende del método de array que fue llamado.

El argumento thisArg (que por defecto es undefined) se usará como el valor de this cuando se llame a callbackFn. El valor this observable por callbackFn se determina según las reglas habituales: si callbackFn no es estricta, los valores primitivos de this se envuelven en objetos, y undefined o null se sustituyen con globalThis. El argumento thisArg es irrelevante para cualquier callbackFn definido con una función flecha, ya que las funciones flecha no tienen su propio enlace this.

El argumento array pasado a callbackFn es útil si se desea leer otro índice durante la iteración, porque puede que no siempre se tenga una variable existente que se refiera al array actual. Generalmente, no se debe mutar el array durante la iteración, pero este argumento también se puede usar para hacerlo. El argumento array no es el array que se está construyendo, en el caso de métodos como map(), filter() y flatMap(); no hay forma de acceder al array que se está construyendo desde la función de callback.

Todos los métodos iterativos son de copia y genéricos, aunque se comportan de manera diferente con los huecos vacíos.

Los siguientes métodos son iterativos: every(), filter(), find(), findIndex(), findLast(), findLastIndex(), flatMap(), forEach(), map(), y some().

En particular, every(), find(), findIndex(), findLast(), findLastIndex(), y some() no siempre invocan callbackFn en cada elemento, ya que detienen la iteración tan pronto como se determina el valor de retorno.

Los métodos reduce() y reduceRight() también toman una función de callback y la ejecutan como máximo una vez por cada elemento del array, pero tienen firmas ligeramente diferentes de los métodos iterativos típicos (por ejemplo, no aceptan thisArg).

El método sort() también toma una función de callback, pero no es un método iterativo. Muta el array en su lugar, no acepta thisArg, y puede invocar la callback múltiples veces en un índice.

Puntos a tener en cuenta:

  1. No todos los métodos hacen la prueba i in this. Los métodos find, findIndex, findLast, y findLastIndex no lo hacen, pero otros métodos sí.

  2. La longitud se memoriza antes de que comience el bucle. Esto afecta a cómo se manejan las inserciones y eliminaciones durante la iteración.

  3. El método no memoriza el contenido del array, por lo que si algún índice se modifica durante la iteración, se podría observar el nuevo valor.

  4. El código anterior itera el array en orden ascendente de índice. Algunos métodos iteran en orden descendente de índice (for (let i = length - 1; i >= 0; i--)): reduceRight(), findLast(), y findLastIndex().

  5. reduce y reduceRight tienen firmas ligeramente diferentes y no siempre comienzan en el primer/último elemento.

Como se ve, este punto detalla las posibilidades e inconvenientes de usar los métodos iterativos que tienen los arrays. Se recomienda consultar en caso de algún comportamiento inesperado durante el uso avanzado. En todo caso siempre se puede usar la estructura for tradicional tipo C y hacerlo todo manualmente.

Array.from()#

Array.from() puede crear un array a partir de cualquier iterable o “array-like” (objetos que parecen arrays).

let arrayLike = {
  0: "Hola",
  1: "Mundo",
  length: 2
};
let arr = Array.from(arrayLike);
console.log(arr); // ["Hola", "Mundo"]
[ "Hola", "Mundo" ]

Se usa frecuentemente Array.from(htmlCollection); aunque actualmente también queda mejor usar [...htmlCollection].

Iterar en Strings#

Una string no es un array de caracteres, pero es un iterable. Por tanto, se puede:

Iterar con for..of

for(let char of "abcde"){ console.log(char);} 

En este caso, trata el string como una secuencia de caracteres unicode. Por eso ocurren curiosidades como en el caso de iterar con caracteres unicode compuestos con ZWJ(zero width joiner):

(()=>{
    let family = []
    for (const person of "👩‍👩‍👧‍👦"){
        family.push(person);
    }
    console.log(family);
    
})();
[
  "👩", "‍",   "👩",
  "‍",   "👧", "‍",
  "👦"
]

Iterar con …

console.log([..."👩‍👩‍👧‍👦"]);
[
  "👩", "‍",   "👩",
  "‍",   "👧", "‍",
  "👦"
]

Convertir en Array

console.log(Array.from("abcd👩‍👩‍👧‍👦"));
[
  "a",  "b", "c",  "d",
  "👩", "‍",  "👩", "‍",
  "👧", "‍",  "👦"
]

Iterables avanzados#

Set#

Un Set es un objeto iterable que representa una colección de valores únicos.

  • No se pueden repetir los mismos valores o referencias. Elimina la repetición, por lo que es un buen método para eliminar repetidos en arrays.

  • Tiene funciones y atributos como .add(), .delete(), clear(), .size o .has().

  • Permite ser recorrido como cualquier iterable.

  • No tiene acceso aleatorio como los arrays porque no tiene índice.

  • .has() es mucho más rápido que buscar en un array con .includes(), así que para búsquedas es mucho más eficiente.

let mySet = new Set();
mySet.add(1);
mySet.add(2);
mySet.add(2); // No se añade, ya que 2 ya está en el set
console.log(mySet.size); // 2

mySet.forEach(value => { console.log(value); }); // 1, 2
console.log([...mySet]); // [1, 2]
2
1
2
[ 1, 2 ]

El nombre de Set es porque, al igual que en Python y otros lenguajes, se trata de una estructura de datos similar al conjunto en las matemáticas. Como se trata de conjuntos de datos, se les puede aplicar operaciones del álgebra de conjuntos, como son:

  • intersection()

  • union()

  • difference()

  • symmetricDifference(): los elementos que están en cualquiera de los dos conjuntos, pero no el los dos.

  • isSubsetOf(): Si es un subconjunto

  • isSupersetOf(): Si es un conjunto que contiene todos los elementos.

  • isDisjointFrom(): Si no tienen elementos en común.

Map#

Un Map es una colección de pares clave-valor donde las claves pueden ser de cualquier tipo.

  • Al contrario que los objetos, las claves pueden ser un objeto.

  • Al contrario que los objetos, tiene un atributo .size, que indica el tamaño.

  • Se puede iterar en un Map de forma natural.

  • No tienen prototipo.

  • Sus funciones son: clear(), delete(key), entries(), get(), has(), keys(), set(key,value), values().

  • Son muy útiles con datos obtenidos de una API o con elementos del DOM.

  • Se podrían usar Maps en vez de objetos para colecciones de datos, ya que son más seguros, robustos y rápidos.

https://www.geeksforgeeks.org/introduction-to-map-data-structure-and-algorithm-tutorials/

let myMap = new Map();
myMap.set('a', 1);
myMap.set('b', 2);

console.log(myMap.get('a')); // 1
console.log(myMap.size); // 2

myMap.forEach((value, key) => { console.log(key, value); }); // 'a' 1, 'b' 2
1
2
a 1
b 2

WeakMap#

Definición y Características:

  • WeakMap es una estructura de datos introducida en ECMAScript 6 (ES6) para almacenar pares clave-valor, donde las claves deben ser objetos y los valores pueden ser cualquier tipo de dato.

  • Claves solo de objetos: Solo acepta objetos como claves. Intentar usar un tipo no objeto como clave resultará en un TypeError.

  • Recolección de basura de claves: Permite que las claves sean recolectadas cuando no hay otras referencias a ellas.

  • No enumeración de claves: No expone métodos para enumerar sus claves (keys(), values(), entries()).

  • Sin propiedad size: No tiene la propiedad size.

Creación y Uso:

  • Se crea utilizando new WeakMap().

  • Métodos disponibles:

    • set(key, value): Establece un par clave-valor.

    • get(key): Recupera el valor asociado a una clave.

    • has(key): Verifica si una clave existe.

    • delete(key): Elimina un par clave-valor.

Casos de Uso Comunes:

  • Almacenamiento de datos privados: Asociar datos privados a objetos sin exponerlos públicamente.

  • Mecanismo de caché: Caché de datos donde los valores pueden ser recolectados si no son necesarios.

  • Gestión de elementos del DOM: Seguimiento de elementos del DOM sin impedir su recolección de basura.

  • Memoización: Almacenar resultados de funciones costosas sin crecimiento indefinido de memoria.

WeakSet#

Definición y Características:

  • WeakSet es una estructura de datos diseñada para trabajar con colecciones de objetos, permitiendo solo objetos como valores y manteniendo referencias débiles.

  • Valores solo de objetos: Solo permite almacenar objetos.

  • Referencias débiles: Las referencias débiles permiten la recolección de basura de los objetos cuando no son necesarios.

  • Sin enumeración: No proporciona métodos para enumerar sus valores (keys(), values(), entries(), forEach()).

  • Sin propiedad size: No tiene la propiedad size.

Creación y Uso:

  • Se crea utilizando new WeakSet().

  • Métodos disponibles:

    • add(value): Añade un objeto al WeakSet.

    • delete(value): Elimina un objeto del WeakSet.

    • has(value): Verifica si un objeto está en el WeakSet.

Casos de Uso Comunes:

  • Comprobación de pertenencia de objetos: Seguimiento de la pertenencia de objetos sin impedir su recolección de basura.

  • Prevención de duplicación de objetos: Asegura que los objetos no se dupliquen dentro de una colección.

  • Gestión de referencias débiles en cachés: Mantener referencias débiles a objetos en cachés.

  • Gestión de referencias de objetos en estructuras de datos: Gestión de referencias de objetos en gráficos o estructuras arbóreas.

Comparativa#

Característica

Maps

Objetos

Sets

Arrays

WeakMap

WeakSet

Recorrer

No

No

No

Valores repetidos

No

No

No

Clave-valor

No

Índices

No

Eliminar elementos

.delete()

delete

.delete()

No directamente

.delete()

.delete()

Filtrar, ordenar, map

No

No

No

No

No

Acceso aleatorio

No

No

Serializables a JSON

No

No

No

No

Objetos: Los objetos son colecciones de pares clave-valor, donde las claves son strings o symbols y los valores pueden ser de cualquier tipo. Se utilizan para representar entidades con propiedades y métodos, como un modelo de datos complejo en una aplicación.

Arrays: Los arrays son listas ordenadas de elementos accesibles por índices numéricos. Son ideales para manejar colecciones de elementos que necesitan ser recorridos secuencialmente o en las que el orden es importante, como listas de tareas o conjuntos de resultados de búsqueda.

Sets: Los sets son colecciones de valores únicos sin claves. No permiten valores duplicados y son útiles cuando se necesita mantener una lista de elementos únicos, como un conjunto de etiquetas de un artículo o una lista de usuarios únicos en una sesión.

Maps: Los maps son similares a los objetos en que almacenan pares clave-valor, pero las claves pueden ser de cualquier tipo, no solo strings o symbols. Además, los maps mantienen el orden de inserción de los elementos, lo que los hace adecuados para almacenar datos donde el orden es importante y las claves no son necesariamente strings, como una tabla de correspondencia entre objetos y sus propiedades.

Los objetos son perfectos para estructuras de datos complejas y entidades, los arrays son ideales para listas ordenadas, los sets son útiles para colecciones de elementos únicos, y los maps son adecuados para pares clave-valor con claves de cualquier tipo y donde el orden de inserción es relevante.

Ejemplo de uso#

// Set
let setExample = new Set([1, 2, 3, 4, 5]);
console.log(setExample.has(3)); // true

// Map
let mapExample = new Map();
mapExample.set('name', 'Alice');
mapExample.set('age', 25);
console.log(mapExample.get('name')); // Alice
true
Alice

El siguiente ejemplo de uso utiliza un Mappara gestionar si una fila de una tabla está seleccionada o no:

let trMap = new Map();

function clickFunction(){
  trMap.get(this).selected = trMap.get(this).selected ? false : true;
  if(trMap.get(this).selected) this.style.backgroundColor = "#F00";
  else this.style.backgroundColor = null;
}

let trs = table.querySelectorAll('tr');
for(let tr of trs){
  trMap.set(tr,{selected: false});
  tr.addEventListener('click',clickFunction);
}

Arrays tipados#

Los arrays tipados permiten manejar y manipular eficientemente datos binarios y números en diferentes formatos, particularmente en aplicaciones que requieren un uso intensivo de datos. Todos estos arrays tipados heredan de una clase base llamada TypedArray, que proporciona una API común para la manipulación de datos en buffers binarios.

Tipo

Rango de valores

Tamaño en bytes

Tipo en Web IDL

Int8Array

-128 a 127

1

byte

Uint8Array

0 a 255

1

octet

Uint8ClampedArray

0 a 255 (sin valores negativos, clampa valores fuera de rango a 0 o 255)

1

octet

Int16Array

-32,768 a 32,767

2

short

Uint16Array

0 a 65,535

2

unsigned short

Int32Array

-2,147,483,648 a 2,147,483,647

4

long

Uint32Array

0 a 4,294,967,295

4

unsigned long

Float16Array

-65,504 a 65,504 (aproximado)

2

N/A (no estandarizado en Web IDL)

Float32Array

-3.4 × 10³⁸ a 3.4 × 10³⁸

4

unrestricted float

Float64Array

-1.8 × 10³⁰⁸ a 1.8 × 10³⁰⁸

8

unrestricted double

BigInt64Array

-2⁶³ a 2⁶³ - 1

8

bigint

BigUint64Array

0 a 2⁶⁴ - 1

8

bigint

Al especificar el tipo y tamaño de cada valor en el array, permiten el acceso y manipulación eficiente de datos sin necesidad de validación de tipo en cada operación, lo que mejora significativamente la velocidad en aplicaciones intensivas como:

  • Gráficos y juegos: Usados en WebGL para renderizar gráficos 3D en la web.

  • Manipulación de audio y video: Útiles en la decodificación de archivos multimedia.

  • Ciencia de datos: Para procesamiento numérico y simulaciones.

  • Aplicaciones de red: Para manejar datos binarios en comunicación de bajo nivel, como sockets de WebRTC.

Un array tipado se crea utilizando el constructor correspondiente, como new Int16Array(length), donde length es el número de elementos del array. También puede crearse desde un ArrayBuffer para trabajar con un bloque de datos binarios compartidos entre varios arrays tipados.

Por ejemplo:

let intArray = new Int16Array(10);
intArray[0] = 12345;
intArray[1] = -12345;
console.log(intArray); // Int16Array [12345, -12345, 0, 0, ..., 0]