Módulo 06: Arrays

Objetivos del módulo

  • Comprender qué es un array y cuándo usarlo
  • Crear arrays de distintas formas: literal, Array.from, Array.of y fill
  • Acceder a sus elementos con índices y con at(), y manejar length
  • Añadir, quitar, buscar y recorrer elementos con los métodos adecuados
  • Transformar datos con map, filter, reduce, flat y flatMap
  • Ordenar correctamente (números, textos con acentos y objetos)
  • Distinguir los métodos que modifican el array de los que devuelven uno nuevo (toSorted, toReversed, toSpliced, with)
  • Usar la desestructuración, el operador spread y la agrupación con Object.groupBy
  • Entender la diferencia entre copia superficial y copia profunda
  • Encadenar métodos para resolver problemas reales con código legible

1. ¿Qué es un array?

📘 Concepto: Un array (también llamado arreglo o vector) es una lista ordenada de valores. Cada valor ocupa una posición numerada llamada índice, que empieza en 0.

Piensa en un array como en una estantería con huecos numerados: en el hueco 0 pones un libro, en el 1 otro, y así sucesivamente. Si quieres el tercer libro, no lo buscas por el título: vas directamente al hueco número 2.

// En lugar de alumno1, alumno2, alumno3... una sola variable para toda la lista
const alumnos = ['Ana', 'Luis', 'Marta'];

console.log(alumnos[0]);      // Ana
console.log(alumnos[2]);      // Marta
console.log(alumnos.length);  // 3

Características de los arrays en JavaScript:

Característica Explicación
Ordenados Los elementos mantienen el orden en que los colocas
Indexados desde 0 El primer elemento es [0] y el último [length - 1]
Dinámicos Crecen y encogen sin declarar un tamaño previo
Heterogéneos Pueden mezclar tipos ([1, 'hola', true]), aunque no es buena práctica
Son objetos typeof [] devuelve 'object'; para comprobar si algo es un array usa Array.isArray(valor)

💡 Consejo: Aunque JavaScript lo permite, procura que todos los elementos de un array sean del mismo tipo (todo números, todo textos o todo objetos con la misma forma). Así el código es más predecible y los métodos como sort o map se comportan como esperas.

📘 Concepto: En este módulo los comentarios muestran los resultados de forma compacta, como [1, 2, 3]. Node.js los escribe con espacios ([ 1, 2, 3 ]) y el navegador añade la longitud ((3) [1, 2, 3]), pero el valor es el mismo.


2. Crear arrays

2.1. Literal de array (la forma habitual)

const vacio = [];
const numeros = [10, 20, 30];
const frutas = ['manzana', 'pera', 'uva'];
const mezcla = [1, 'dos', { tres: 3 }, [4]];
const colores = ['rojo', 'verde', 'azul',];  // la coma final está permitida
console.log(colores.length);  // 3

2.2. El constructor Array y su trampa

const a = new Array(1, 2, 3);  // [1, 2, 3]
const b = new Array(3);        // ¡No es [3]! Es un array con 3 huecos vacíos
console.log(b);                // [ <3 empty items> ]
console.log(b.length);         // 3

⚠️ Importante: new Array(n) con un único número crea un array de longitud n lleno de huecos (empty slots), no un array con ese número. Es una fuente clásica de errores. Usa literales o Array.of.

2.3. Array.of

Crea un array con los argumentos que le pases, sin la trampa anterior:

console.log(Array.of(3));        // [3]
console.log(Array.of(1, 2, 3));  // [1, 2, 3]

2.4. Array.from: crear arrays a partir de otras cosas

Array.from convierte en array cualquier iterable (cadenas, Set, Map…) o cualquier objeto “parecido a un array” (con length). Además acepta una función de mapeo como segundo argumento.

// Desde una cadena: un elemento por carácter
console.log(Array.from('hola'));  // ['h', 'o', 'l', 'a']

// Desde un Set (elimina duplicados de paso)
console.log(Array.from(new Set([1, 1, 2, 3, 3])));  // [1, 2, 3]

// Desde un objeto con length: generar secuencias
console.log(Array.from({ length: 5 }, (_, i) => i));      // [0, 1, 2, 3, 4]
console.log(Array.from({ length: 5 }, (_, i) => i + 1));  // [1, 2, 3, 4, 5]

💡 Consejo: El patrón Array.from({ length: n }, (_, i) => ...) es la forma idiomática de generar arrays de un tamaño concreto. El guion bajo _ indica que ignoramos el primer parámetro (el valor, que es undefined).

2.5. fill: rellenar con un valor

const ceros = new Array(5).fill(0);
console.log(ceros);  // [0, 0, 0, 0, 0]
console.log([1, 2, 3, 4, 5].fill(9, 1, 3));  // [1, 9, 9, 4, 5]  (fill(valor, inicio, fin) sin incluir fin)

⚠️ Importante: fill coloca el mismo valor en todas las posiciones. Si ese valor es un objeto o un array, todas las posiciones apuntan al mismo objeto:

const malo = new Array(3).fill([]);
malo[0].push('x');
console.log(malo);  // [['x'], ['x'], ['x']]  ← ¡los tres son el mismo array!

const bueno = Array.from({ length: 3 }, () => []);
bueno[0].push('x');
console.log(bueno);  // [['x'], [], []]

3. Acceder a los elementos

3.1. Índices y at()

const letras = ['a', 'b', 'c', 'd', 'e'];

console.log(letras[0]);                  // a
console.log(letras[letras.length - 1]);  // e   (último, forma clásica)
console.log(letras[10]);                 // undefined (no existe, no da error)

// at() admite índices negativos: -1 es el último, -2 el penúltimo...
console.log(letras.at(0));   // a
console.log(letras.at(-1));  // e
console.log(letras.at(-2));  // d

📘 Concepto: at(indice) (ES2022) hace lo mismo que los corchetes pero admite índices negativos que cuentan desde el final. Con corchetes, letras[-1] devuelve undefined, porque busca una propiedad llamada "-1".

3.2. Modificar por índice

const notas = [5, 7, 9];
notas[1] = 8;
console.log(notas);  // [5, 8, 9]

// Asignar más allá del final crea huecos (evítalo)
notas[5] = 10;
console.log(notas);         // [5, 8, 9, <2 empty items>, 10]
console.log(notas.length);  // 6

3.3. La propiedad length

length siempre vale el índice más alto + 1. Además se puede escribir: si la reduces, recortas el array.

const lista = [1, 2, 3, 4, 5];
lista.length = 2;
console.log(lista);  // [1, 2]

lista.length = 0;    // forma rápida de vaciar un array (lo modifica)
console.log(lista);  // []

⚠️ Importante: const impide reasignar la variable, no modificar el contenido. Con const lista = [...] puedes hacer lista.push(6) o lista.length = 0, pero no lista = [].


4. Añadir y quitar elementos

Método Qué hace Devuelve ¿Modifica el array?
push(...elementos) Añade al final Nueva longitud Sí
pop() Quita el último El elemento quitado Sí
unshift(...elementos) Añade al principio Nueva longitud Sí
shift() Quita el primero El elemento quitado Sí
splice(inicio, cuantos, ...nuevos) Quita y/o inserta en cualquier posición Array con los quitados Sí
const cola = ['Ana', 'Luis'];

cola.push('Marta');            // al final
console.log(cola);             // ['Ana', 'Luis', 'Marta']

cola.unshift('Pedro');         // al principio
console.log(cola);             // ['Pedro', 'Ana', 'Luis', 'Marta']

const ultimo = cola.pop();     // quita el último
const primero = cola.shift();  // quita el primero
console.log(ultimo, primero);  // Marta Pedro
console.log(cola);             // ['Ana', 'Luis']
console.log(cola.push('Eva', 'Raúl'));  // 4 (admite varios y devuelve la nueva longitud)

💡 Consejo: Truco para recordarlo: push/pop trabajan por el final (como una pila de platos), shift/unshift por el principio. shift y unshift son más lentos en arrays enormes porque tienen que renumerar todos los elementos.

4.1. splice: la navaja suiza

splice(inicio, cuantosBorrar, ...elementosAInsertar) modifica el array en cualquier posición:

const meses = ['ene', 'feb', 'abr', 'may', 'jun'];

// Insertar 'mar' en la posición 2 sin borrar nada
meses.splice(2, 0, 'mar');
console.log(meses);  // ['ene', 'feb', 'mar', 'abr', 'may', 'jun']

// Borrar 2 elementos desde la posición 4
const borrados = meses.splice(4, 2);
console.log(borrados);  // ['may', 'jun']
console.log(meses);     // ['ene', 'feb', 'mar', 'abr']

// Reemplazar: borrar 1 en la posición 0 e insertar otro
meses.splice(0, 1, 'enero');
console.log(meses);  // ['enero', 'feb', 'mar', 'abr']

✏️ Ejercicio: Partiendo de const dias = ['lun', 'mar', 'jue', 'vie'], usa un único splice para insertar 'mié' en su sitio. Después, con otro splice, sustituye 'vie' por 'viernes'.

4.2. Eliminar un elemento concreto

const tareas = ['comprar', 'estudiar', 'correr'];
const posicion = tareas.indexOf('estudiar');
if (posicion !== -1) {
  tareas.splice(posicion, 1);
}
console.log(tareas);  // ['comprar', 'correr']
// Alternativa sin modificar el original: tareas.filter((t) => t !== 'estudiar') (sección 7)

⚠️ Importante: No uses delete tareas[1]: deja un hueco y no cambia length. Para quitar elementos usa splice o filter.


5. Buscar en un array

5.1. Por valor: indexOf, lastIndexOf e includes

const numeros = [3, 7, 3, 9, NaN];

console.log(numeros.indexOf(3));      // 0   (primera aparición)
console.log(numeros.lastIndexOf(3));  // 2   (última aparición)
console.log(numeros.indexOf(100));    // -1  (no está)
console.log(numeros.includes(9));     // true
// Diferencia sutil con NaN
console.log(numeros.indexOf(NaN));    // -1   (usa ===, y NaN !== NaN)
console.log(numeros.includes(NaN));   // true (usa SameValueZero)

💡 Consejo: Si solo quieres saber si está, usa includes: se lee mejor que indexOf(x) !== -1 y funciona con NaN.

5.2. Por condición: find, findIndex, findLast y findLastIndex

Cuando buscas “el primer elemento que cumpla algo”, pasas una función (un callback) que devuelve true o false:

const productos = [
  { id: 1, nombre: 'Teclado', precio: 25 },
  { id: 2, nombre: 'Ratón', precio: 15 },
  { id: 3, nombre: 'Monitor', precio: 180 },
  { id: 4, nombre: 'Alfombrilla', precio: 8 },
];

const caro = productos.find((p) => p.precio > 100);
console.log(caro);  // { id: 3, nombre: 'Monitor', precio: 180 }

const indiceRaton = productos.findIndex((p) => p.nombre === 'Ratón');
console.log(indiceRaton);  // 1

// Desde el final (ES2023)
const ultimoBarato = productos.findLast((p) => p.precio < 20);
console.log(ultimoBarato.nombre);  // Alfombrilla
console.log(productos.findLastIndex((p) => p.precio < 20));  // 3
console.log(productos.find((p) => p.precio > 1000));        // undefined (no hay ninguno)

5.3. Preguntas de sí o no: some y every

const edades = [17, 22, 35, 41];

console.log(edades.some((e) => e < 18));    // true  (¿alguno es menor?)
console.log(edades.every((e) => e >= 18));  // false (¿todos son mayores?)
console.log([].every((e) => e > 0));        // true (en un array vacío nadie incumple)

5.4. ¿Qué método de búsqueda uso?

Quiero… Método Devuelve si no hay
Saber si un valor está includes(valor) false
La posición de un valor indexOf(valor) / lastIndexOf(valor) -1
El primer elemento que cumple una condición find(fn) undefined
La posición del primero que cumple findIndex(fn) -1
El último que cumple / su posición findLast(fn) / findLastIndex(fn) undefined / -1
Saber si alguno cumple some(fn) false
Saber si todos cumplen every(fn) true (array vacío)
Todos los que cumplen filter(fn) (sección 7) []

💡 Consejo: find, findIndex, some y every se detienen en cuanto conocen la respuesta. filter siempre recorre el array entero. Si solo necesitas uno, usa find.


6. Recorrer un array

const ciudades = ['Madrid', 'Sevilla', 'Bilbao'];

// 1) for...of: el más legible cuando solo necesitas el valor
for (const ciudad of ciudades) {
  console.log(ciudad);
}

// 2) for...of con entries(): valor e índice a la vez
for (const [indice, ciudad] of ciudades.entries()) {
  console.log(`${indice + 1}. ${ciudad}`);
}
// 1. Madrid
// 2. Sevilla
// 3. Bilbao

// 3) forEach: ejecuta una función por elemento
ciudades.forEach((ciudad, indice) => {
  console.log(`${indice}: ${ciudad}`);
});
Forma ¿Admite break/continue? ¿Funciona con await dentro? Cuándo usarla
for (let i...) Sí Sí Necesitas el índice, saltos o recorrer al revés
for...of Sí Sí Opción por defecto, la más legible
forEach No No espera (cada vuelta lanza su promesa) Efectos sencillos encadenados tras otros métodos
for...in Sí Sí No para arrays: recorre claves (como texto) y propiedades heredadas

⚠️ Importante: forEach no se puede interrumpir con break y siempre devuelve undefined. Si necesitas parar al encontrar algo, usa for...of con break, o directamente find/some.

⚠️ Importante: No uses for...in con arrays. Está pensado para las claves de un objeto (módulo 07), devuelve los índices como cadenas ('0', '1'…) y puede incluir propiedades añadidas al prototipo.

Además de entries(), existen keys() (solo índices) y values() (solo valores). Los tres devuelven iteradores, que estudiarás a fondo en el módulo 11.


7. Transformar: map, filter y reduce

Estos tres métodos son el corazón del JavaScript moderno. Ninguno modifica el array original: devuelven un valor nuevo.

flowchart LR
    A["[1, 2, 3, 4]"] -->|"map(n => n * 10)"| B["[10, 20, 30, 40]"]
    A -->|"filter(n => n % 2 === 0)"| C["[2, 4]"]
    A -->|"reduce((acc, n) => acc + n, 0)"| D["10"]

7.1. map: transformar cada elemento

map aplica una función a cada elemento y devuelve un array nuevo del mismo tamaño con los resultados.

const precios = [10, 25, 40];
const conIva = precios.map((precio) => precio * 1.21);
console.log(conIva);    // [12.1, 30.25, 48.4]
console.log(precios);   // [10, 25, 40]  (el original no cambia)

const usuarios = [
  { nombre: 'Ana', edad: 28 },
  { nombre: 'Luis', edad: 34 },
];
const nombres = usuarios.map((u) => u.nombre);
console.log(nombres);  // ['Ana', 'Luis']
// El callback recibe (elemento, índice, array)
console.log(nombres.map((nombre, i) => `${i + 1}-${nombre}`));  // ['1-Ana', '2-Luis']

⚠️ Importante: Si en una arrow function con llaves olvidas el return, map devuelve undefined en cada posición. Y para devolver un objeto sin llaves, envuélvelo en paréntesis:

const mal = [1, 2].map((n) => { n * 2; });          // [undefined, undefined]
const bien = [1, 2].map((n) => n * 2);              // [2, 4]
const objetos = [1, 2].map((n) => ({ valor: n }));  // [{ valor: 1 }, { valor: 2 }]
console.log(mal, bien, objetos);

7.2. filter: quedarse con los que cumplen

const notas = [4.5, 7, 3, 9.5, 6, 5];

const aprobadas = notas.filter((nota) => nota >= 5);
console.log(aprobadas);  // [7, 9.5, 6, 5]

// Truco: eliminar valores "falsy" (0, '', null, undefined, NaN, false)
const sucios = ['a', '', null, 'b', undefined, 0, 'c'];
console.log(sucios.filter(Boolean));  // ['a', 'b', 'c']

7.3. reduce: reducir a un único valor

reduce recorre el array llevando un acumulador. En cada vuelta, el callback recibe el acumulador y el elemento actual, y devuelve el nuevo acumulador.

📘 Concepto: Imagina una hucha: empiezas con 0 € (el valor inicial) y, por cada moneda (elemento), la hucha pasa a valer lo que tenía más la moneda. Al final, reduce te devuelve la hucha.

const numeros = [5, 10, 15];

const suma = numeros.reduce((acumulado, n) => acumulado + n, 0);
console.log(suma);  // 30

// Paso a paso:
// vuelta 1: acumulado = 0,  n = 5  → 5
// vuelta 2: acumulado = 5,  n = 10 → 15
// vuelta 3: acumulado = 15, n = 15 → 30

// Contar ocurrencias: el acumulador puede ser un objeto
const votos = ['sí', 'no', 'sí', 'sí', 'abstención', 'no'];
const recuento = votos.reduce((acc, voto) => {
  acc[voto] = (acc[voto] ?? 0) + 1;
  return acc;
}, {});
console.log(recuento);  // { 'sí': 3, no: 2, 'abstención': 1 }

⚠️ Importante: Pasa siempre el valor inicial (el segundo argumento). Sin él, reduce usa el primer elemento como acumulador y, si el array está vacío, lanza un error:

console.log([].reduce((a, b) => a + b, 0));  // 0
try {
  [].reduce((a, b) => a + b);
} catch (error) {
  console.log(error.message);  // Reduce of empty array with no initial value
}

reduceRight hace lo mismo pero recorriendo de derecha a izquierda: ['c', 'b', 'a'].reduceRight((acc, l) => acc + l, '') devuelve 'abc'.

💡 Consejo: reduce es muy potente, pero no lo uses para todo. Si un map, un filter o un bucle for...of expresan la idea con más claridad, prefiérelos. El código se lee muchas más veces de las que se escribe.

7.4. flat y flatMap: aplanar

const anidado = [1, [2, 3], [4, [5, 6]]];

console.log(anidado.flat());          // [1, 2, 3, 4, [5, 6]]  (profundidad 1)
console.log(anidado.flat(2));         // [1, 2, 3, 4, 5, 6]
console.log(anidado.flat(Infinity));  // [1, 2, 3, 4, 5, 6]   (todos los niveles)

// flatMap = map + flat(1): útil cuando cada elemento produce varios
const frases = ['hola mundo', 'buenos días'];
console.log(frases.map((f) => f.split(' ')));      // [['hola', 'mundo'], ['buenos', 'días']]
console.log(frases.flatMap((f) => f.split(' ')));  // ['hola', 'mundo', 'buenos', 'días']

// flatMap también permite filtrar: devolver [] descarta el elemento
console.log([1, -2, 3].flatMap((n) => (n > 0 ? [n] : [])));  // [1, 3]

8. Ordenar arrays

8.1. El peligro de sort() sin argumentos

⚠️ Importante: Por defecto, sort() convierte los elementos a texto y los ordena alfabéticamente por sus códigos Unicode. Con números, el resultado es sorprendente:

const numeros = [10, 1, 5, 100, 25];
numeros.sort();
console.log(numeros);  // [1, 10, 100, 25, 5]  ← ¡'100' va antes que '25' como texto!

8.2. La función comparadora

Para ordenar bien, pasa una función (a, b) que devuelva:

Valor devuelto Significado
Negativo a va antes que b
Positivo a va después que b
0 Mantienen su orden relativo (el ordenamiento es estable desde ES2019)
const numeros = [10, 1, 5, 100, 25];

numeros.sort((a, b) => a - b);  // ascendente
console.log(numeros);           // [1, 5, 10, 25, 100]

numeros.sort((a, b) => b - a);  // descendente
console.log(numeros);           // [100, 25, 10, 5, 1]

8.3. Ordenar textos con acentos: localeCompare

const palabras = ['árbol', 'zapato', 'avión', 'Ébano', 'abeja'];

console.log([...palabras].sort());
// ['abeja', 'avión', 'zapato', 'Ébano', 'árbol']  ← los acentos van al final

console.log([...palabras].sort((a, b) => a.localeCompare(b, 'es')));
// ['abeja', 'árbol', 'avión', 'Ébano', 'zapato']  ← orden correcto en español

localeCompare compara según las reglas del idioma. Para ordenar listas grandes, Intl.Collator es más eficiente y permite opciones, como ordenar números dentro del texto de forma natural:

const ficheros = ['tema10.pdf', 'tema2.pdf', 'tema1.pdf'];
const collator = new Intl.Collator('es', { numeric: true });
console.log(ficheros.sort(collator.compare));  // ['tema1.pdf', 'tema2.pdf', 'tema10.pdf']

8.4. Ordenar objetos

const alumnos = [
  { nombre: 'Lucía', nota: 8.5 },
  { nombre: 'Álvaro', nota: 6 },
  { nombre: 'Marcos', nota: 8.5 },
  { nombre: 'Elena', nota: 9.2 },
];

// Por nota descendente y, a igual nota, por nombre
alumnos.sort((a, b) => b.nota - a.nota || a.nombre.localeCompare(b.nombre, 'es'));
console.log(alumnos.map((a) => `${a.nombre} (${a.nota})`));
// ['Elena (9.2)', 'Lucía (8.5)', 'Marcos (8.5)', 'Álvaro (6)']

💡 Consejo: El truco criterio1 || criterio2 funciona porque, si el primer criterio da 0 (empate), || pasa al segundo. Para invertir el orden actual de un array, sin ordenar, existe reverse().

⚠️ Importante: sort y reverse modifican el array original y además lo devuelven. Esto confunde: const ordenado = lista.sort() ordena también lista. La solución está en la sección siguiente.


9. Métodos inmutables de ES2023

📘 Concepto: Un método mutable cambia el array sobre el que lo llamas. Un método inmutable deja el original intacto y devuelve una copia con el cambio. Las bibliotecas como React o el código que comparte datos entre partes de la aplicación agradecen mucho la inmutabilidad: si nadie modifica los datos “por detrás”, los errores son más fáciles de encontrar.

ES2023 añadió versiones inmutables de los métodos mutables más usados:

const original = [3, 1, 2];

const ordenado = original.toSorted((a, b) => a - b);
const invertido = original.toReversed();
const sinPrimero = original.toSpliced(0, 1);         // como splice, pero devuelve la copia
const conCambio = original.with(1, 99);              // como original[1] = 99, en copia

console.log(original);    // [3, 1, 2]  ← intacto
console.log(ordenado);    // [1, 2, 3]
console.log(invertido);   // [2, 1, 3]
console.log(sinPrimero);  // [1, 2]
console.log(conCambio);   // [3, 99, 2]
console.log(original.with(-1, 0));  // [3, 1, 0]  (admite índices negativos)
// original.with(10, 0) lanza RangeError: with no admite índices fuera de rango

Tabla: métodos mutables frente a inmutables

Mutable (modifica el original) Inmutable (devuelve una copia)
sort(fn) toSorted(fn)
reverse() toReversed()
splice(inicio, n, ...nuevos) toSpliced(inicio, n, ...nuevos)
arr[i] = valor with(i, valor)
push(x) [...arr, x] o concat(x)
unshift(x) [x, ...arr]
pop() / shift() slice(0, -1) / slice(1)
fill(valor) Array.from(arr, () => valor)
copyWithin(...) (sin equivalente directo)

Otros métodos que nunca modifican el array: map, filter, reduce, flat, flatMap, slice, concat, join, at, includes, find, some, every…

💡 Consejo: En código nuevo, prefiere toSorted y compañía salvo que necesites modificar el array a propósito (por rendimiento con millones de elementos, por ejemplo).


10. slice, concat, spread y join

10.1. slice frente a splice

Sus nombres se parecen, pero son muy distintos:

  slice(inicio, fin) splice(inicio, cuantos, ...nuevos)
¿Modifica el original? No Sí
Devuelve Un trozo (copia) Los elementos eliminados
Segundo parámetro Índice final (no incluido) Cantidad a eliminar
Uso típico Copiar o extraer un tramo Insertar, borrar o reemplazar en el sitio
const letras = ['a', 'b', 'c', 'd', 'e'];

console.log(letras.slice(1, 3));  // ['b', 'c']   (del 1 al 3, sin incluir el 3)
console.log(letras.slice(2));     // ['c', 'd', 'e']
console.log(letras.slice(-2));    // ['d', 'e']   (los dos últimos)
console.log(letras.slice());      // copia completa
console.log(letras);              // ['a', 'b', 'c', 'd', 'e']  (intacto)

const quitados = letras.splice(1, 3);
console.log(quitados);  // ['b', 'c', 'd']
console.log(letras);    // ['a', 'e']  (¡modificado!)

💡 Consejo: Regla mnemotécnica: splice cambia el array; slice solo corta una loncha (en inglés, a slice) y deja el original igual.

10.2. Unir arrays: concat y el operador spread

const frutas = ['manzana', 'pera'];
const verduras = ['lechuga', 'tomate'];

const compra1 = frutas.concat(verduras, ['pan']);
const compra2 = [...frutas, ...verduras, 'pan'];  // spread: más flexible
const compra3 = ['leche', ...frutas, 'huevos'];   // insertar en medio

console.log(compra2);  // ['manzana', 'pera', 'lechuga', 'tomate', 'pan']
console.log(compra3);  // ['leche', 'manzana', 'pera', 'huevos']

// Spread también sirve para pasar un array como argumentos sueltos
const temperaturas = [18, 23, 15, 27];
console.log(Math.max(...temperaturas));  // 27

📘 Concepto: El operador spread (...) “despliega” los elementos de un iterable. Ya lo viste con los parámetros rest en el módulo 05: la sintaxis es la misma, pero rest recoge elementos y spread los reparte.

10.3. join: de array a texto

const palabras = ['JavaScript', 'es', 'genial'];

console.log(palabras.join(' '));   // JavaScript es genial
console.log(palabras.join('-'));   // JavaScript-es-genial
console.log(palabras.join());      // JavaScript,es,genial  (coma por defecto)

// null y undefined se convierten en cadena vacía
console.log(['a', null, 'b', undefined].join('|'));  // a||b|

// La operación inversa es split (módulo 08)
console.log('2026-10-03'.split('-'));  // ['2026', '10', '03']

⚠️ Importante: No uses + con arrays: los convierte a texto. [1, 2] + [3] da la cadena '1,23', no [1, 2, 3].


11. Desestructuración de arrays

📘 Concepto: La desestructuración extrae valores de un array y los guarda en variables en una sola línea, usando la posición.

const coordenadas = [40.41, -3.70, 650];

// Sin desestructurar
const latitud1 = coordenadas[0];
const longitud1 = coordenadas[1];

// Desestructurando
const [latitud, longitud, altitud] = coordenadas;
console.log(latitud, longitud, altitud);  // 40.41 -3.7 650

11.1. Saltar elementos, valores por defecto y rest

const podio = ['Ana', 'Luis', 'Marta', 'Pedro', 'Eva'];

// Saltar posiciones dejando huecos entre comas
const [oro, , bronce] = podio;
console.log(oro, bronce);  // Ana Marta

// Rest: recoger "el resto" en un array (debe ir al final)
const [ganador, ...demas] = podio;
console.log(ganador);  // Ana
console.log(demas);    // ['Luis', 'Marta', 'Pedro', 'Eva']

// Valores por defecto (se usan si el valor es undefined)
const [a = 1, b = 2, c = 3] = [10, undefined];
console.log(a, b, c);  // 10 2 3

11.2. Intercambiar variables

let x = 1;
let y = 2;
[x, y] = [y, x];
console.log(x, y);  // 2 1

11.3. En parámetros y con funciones que devuelven arrays

// Una función puede devolver varios valores en un array
function minMax(numeros) {
  return [Math.min(...numeros), Math.max(...numeros)];
}
const [minimo, maximo] = minMax([4, 9, 1, 7]);
console.log(minimo, maximo);  // 1 9

// Desestructurar en los parámetros del callback
console.log([[1, 2], [3, 4]].map(([px, py]) => px + py));  // [3, 7]

12. Agrupar: Object.groupBy y Map.groupBy

Agrupar elementos según un criterio es una tarea tan común que ES2024 incorporó dos funciones específicas. Antes había que hacerlo con reduce.

const inventario = [
  { nombre: 'espárragos', tipo: 'verdura', cantidad: 5 },
  { nombre: 'plátanos', tipo: 'fruta', cantidad: 0 },
  { nombre: 'cabra', tipo: 'carne', cantidad: 23 },
  { nombre: 'cerezas', tipo: 'fruta', cantidad: 5 },
  { nombre: 'pescado', tipo: 'carne', cantidad: 22 },
];

const porTipo = Object.groupBy(inventario, (producto) => producto.tipo);
console.log(Object.keys(porTipo));               // ['verdura', 'fruta', 'carne']
console.log(porTipo.fruta.map((p) => p.nombre));  // ['plátanos', 'cerezas']

⚠️ Importante: Object.groupBy es un método estático de Object (no inventario.groupBy(...)) y devuelve un objeto sin prototipo (Object.create(null)). Por eso Node lo muestra como [Object: null prototype] {...} y no tiene métodos como hasOwnProperty; usa Object.hasOwn(porTipo, 'fruta') si lo necesitas.

Map.groupBy hace lo mismo pero devuelve un Map (módulo 11), cuyas claves pueden ser de cualquier tipo, por ejemplo objetos:

const aprobados = { etiqueta: 'aprobados' };
const suspensos = { etiqueta: 'suspensos' };

const notas = [7, 3, 9, 4.5, 5];
const grupos = Map.groupBy(notas, (n) => (n >= 5 ? aprobados : suspensos));
console.log(grupos.get(aprobados));  // [7, 9, 5]
console.log(grupos.get(suspensos));  // [3, 4.5]

13. Array.fromAsync (ES2026)

Array.fromAsync es la versión asíncrona de Array.from: crea un array a partir de un iterable asíncrono o de un iterable de promesas, esperando a cada valor. Devuelve una promesa, por eso se usa con await.

const esperar = (ms, valor) => new Promise((resolver) => setTimeout(() => resolver(valor), ms));

const promesas = [esperar(30, 'a'), esperar(10, 'b'), esperar(20, 'c')];
const valores = await Array.fromAsync(promesas);
console.log(valores);  // ['a', 'b', 'c']  (en el orden original)

📘 Concepto: No te preocupes si Promise, async y await te resultan extraños: los estudiarás en el módulo 12 (Asincronía), donde volveremos a Array.fromAsync con generadores asíncronos. El await “suelto” del ejemplo funciona porque los módulos ES admiten top-level await.


14. Arrays multidimensionales

Un array puede contener otros arrays. Así se representan tablas, tableros o matrices:

// Tablero de tres en raya: 3 filas × 3 columnas
const tablero = [
  ['X', 'O', 'X'],
  ['O', 'X', ' '],
  [' ', ' ', 'O'],
];

console.log(tablero[0][2]);  // X   (fila 0, columna 2)
tablero[2][0] = 'X';         // colocar ficha

// Recorrer filas y columnas
for (const fila of tablero) {
  console.log(fila.join(' | '));
}
// X | O | X
// O | X |
// X |   | O

Crear una matriz de tamaño dado (recuerda la trampa de fill con objetos):

const filas = 3;
const columnas = 4;
const matriz = Array.from({ length: filas }, () => new Array(columnas).fill(0));
matriz[1][2] = 7;
console.log(matriz);  // [[0, 0, 0, 0], [0, 0, 7, 0], [0, 0, 0, 0]]

// Transponer (filas ↔ columnas) con map
const datos = [[1, 2, 3], [4, 5, 6]];
const transpuesta = datos[0].map((_, col) => datos.map((fila) => fila[col]));
console.log(transpuesta);  // [[1, 4], [2, 5], [3, 6]]

✏️ Ejercicio: Escribe una función que reciba una matriz numérica y devuelva un array con la suma de cada fila. Hazlo con map y reduce.

💡 Consejo: Para ver una matriz o un array de objetos de forma clara en el navegador o en Node, usa console.table(matriz).


15. Copia superficial frente a copia profunda

📘 Concepto: Los arrays se guardan por referencia. Si haces const b = a, no copias el array: tienes dos nombres para el mismo array, como dos llaves de la misma casa.

const a = [1, 2, 3];
const b = a;        // misma referencia
b.push(4);
console.log(a);  // [1, 2, 3, 4]  ← ¡también cambió! (y a === b es true)

15.1. Copia superficial (shallow copy)

Crea un array nuevo, pero los elementos que sean objetos siguen compartidos:

const original = [1, 2, 3];
const copia1 = [...original];  // también: original.slice() o Array.from(original)
copia1.push(99);
console.log(original);  // [1, 2, 3]  (intacto)

// El problema aparece con objetos dentro
const usuarios = [{ nombre: 'Ana' }, { nombre: 'Luis' }];
const copiaSuperficial = [...usuarios];
copiaSuperficial[0].nombre = 'ANA';
console.log(usuarios[0].nombre);  // ANA  ← el objeto interior es el mismo

15.2. Copia profunda (deep copy) con structuredClone

const usuarios = [{ nombre: 'Ana', aficiones: ['leer'] }];
const copiaProfunda = structuredClone(usuarios);

copiaProfunda[0].nombre = 'ANA';
copiaProfunda[0].aficiones.push('nadar');

console.log(usuarios);       // [{ nombre: 'Ana', aficiones: ['leer'] }]
console.log(copiaProfunda);  // [{ nombre: 'ANA', aficiones: ['leer', 'nadar'] }]
Técnica Tipo de copia Notas
const b = a Ninguna (misma referencia) Solo otro nombre
[...a], a.slice(), Array.from(a) Superficial Suficiente si los elementos son primitivos
structuredClone(a) Profunda Copia objetos anidados, Date, Map, Set… No copia funciones
JSON.parse(JSON.stringify(a)) Profunda (limitada) Truco antiguo: pierde undefined, fechas, Map… Evítalo

💡 Consejo: Profundizaremos en referencias y structuredClone en el módulo 07 (Objetos).


16. Encadenamiento de métodos: ejemplos reales

Como map, filter, toSorted y compañía devuelven arrays, puedes encadenarlos formando una “tubería” de transformaciones. Escribe cada paso en su propia línea para que se lea de arriba abajo.

16.1. Carrito de la compra

const carrito = [
  { producto: 'Camiseta', precio: 19.99, unidades: 2, categoria: 'ropa' },
  { producto: 'Auriculares', precio: 59.9, unidades: 1, categoria: 'electrónica' },
  { producto: 'Calcetines', precio: 4.5, unidades: 0, categoria: 'ropa' },
  { producto: 'Cable USB-C', precio: 9.95, unidades: 3, categoria: 'electrónica' },
];

// 1) Líneas con unidades, 2) importe por línea, 3) ordenadas de mayor a menor
const lineas = carrito
  .filter((item) => item.unidades > 0)
  .map((item) => ({ ...item, importe: item.precio * item.unidades }))
  .toSorted((a, b) => b.importe - a.importe);

for (const { producto, unidades, importe } of lineas) {
  console.log(`${producto.padEnd(12)} x${unidades}  ${importe.toFixed(2)} €`);
}
// Auriculares  x1  59.90 €
// Camiseta     x2  39.98 €
// Cable USB-C  x3  29.85 €

const total = lineas.reduce((suma, linea) => suma + linea.importe, 0);
console.log(`Total: ${total.toFixed(2)} €`);  // Total: 129.73 €

⚠️ Importante: Los números decimales en JavaScript tienen errores de redondeo (0.1 + 0.2 no es exactamente 0.3). Por eso mostramos los importes con toFixed(2). En aplicaciones de facturación reales se trabaja en céntimos enteros o con bibliotecas decimales.

16.2. Notas de alumnos

const alumnos = [
  { nombre: 'Lucía', notas: [8, 9, 7.5] },
  { nombre: 'Álvaro', notas: [4, 5.5, 3] },
  { nombre: 'Marcos', notas: [6, 7, 6.5] },
  { nombre: 'Elena', notas: [9.5, 10, 9] },
  { nombre: 'Iker', notas: [] },
];

const media = (numeros) => numeros.reduce((s, n) => s + n, 0) / numeros.length;

const informe = alumnos
  .filter(({ notas }) => notas.length > 0)           // descartar sin notas
  .map(({ nombre, notas }) => ({ nombre, media: media(notas) }))
  .toSorted((a, b) => b.media - a.media)
  .map(({ nombre, media: m }, i) => `${i + 1}. ${nombre}: ${m.toFixed(2)}${m >= 5 ? '' : ' (suspenso)'}`);

console.log(informe.join('\n'));
// 1. Elena: 9.50
// 2. Lucía: 8.17
// 3. Marcos: 6.50
// 4. Álvaro: 4.17 (suspenso)
console.log(Math.max(...alumnos.flatMap((a) => a.notas)));  // 10 (nota más alta del grupo)

✏️ Ejercicio: Añade al informe de notas una línea final con el porcentaje de aprobados (media ≥ 5) usando filter y length.


17. Rendimiento y legibilidad

  • La legibilidad va primero. Una cadena filter → map → reduce con nombres claros es más fácil de mantener que un bucle con cinco variables. Solo optimiza cuando midas un problema real.
  • Cada método encadenado recorre el array y crea uno nuevo. Con 100 elementos da igual; con millones de elementos en un bucle crítico, un único for...of o reduce puede ser más eficiente.
  • Usa el método más específico: find en lugar de filter(...)[0], some en lugar de filter(...).length > 0, includes en lugar de indexOf(...) !== -1. Expresan mejor la intención y se detienen antes.
  • Buscar en arrays grandes es lento (includes y find recorren elemento a elemento). Si vas a consultar muchas veces “¿está este valor?”, convierte el array en un Set; si buscas por id, usa un Map (módulo 11).
  • No mutes los datos que recibes en una función: usa toSorted, with, spread… Mutar datos compartidos es una de las fuentes de errores más difíciles de rastrear.
  • No dejes huecos (new Array(n) sin rellenar, delete arr[i], arr[100] = x): los métodos tratan los huecos de forma inconsistente (forEach los salta, for...of los ve como undefined).

💡 Consejo: Para medir de forma rápida, rodea el código con console.time('etiqueta') y console.timeEnd('etiqueta'). Por ejemplo, 1000 llamadas a includes sobre un array de 100 000 números tardan cientos de veces más que 1000 llamadas a has sobre un Set con los mismos datos.


18. Ejercicios

Ejercicio 1: Estadísticas básicas

Dado const temperaturas = [22, 25, 19, 30, 28, 17, 24], calcula y muestra: la temperatura máxima, la mínima, la media con un decimal y cuántos días superaron los 24 grados. Usa Math.max/Math.min con spread, reduce y filter.

Ejercicio 2: Gestor de lista de tareas

Crea un array tareas vacío y escribe las funciones anadir(texto), completar(indice) (marca la tarea como hecha), eliminar(indice) y listar(), que muestre algo como 1. [x] Comprar pan. Cada tarea es un objeto { texto, hecha }. Usa push, splice y entries().

Ejercicio 3: Ordenación múltiple sin mutar

Con el array de alumnos de la sección 8.4, obtén sin modificar el original (usa toSorted): (a) los alumnos ordenados alfabéticamente respetando los acentos; (b) ordenados por nota ascendente; (c) los tres mejores. Comprueba al final con console.log que el array original no ha cambiado.

Ejercicio 4: Eliminar duplicados y contar

Dado const visitas = ['inicio', 'blog', 'inicio', 'contacto', 'blog', 'inicio'], obtén: (a) la lista de páginas sin repetir (con Set y spread); (b) un objeto con el número de visitas por página usando reduce; (c) la página más visitada.

Ejercicio 5: Agrupar pedidos

Tienes un array de pedidos { id, cliente, total, estado } donde estado es 'pendiente', 'enviado' o 'entregado'. Crea al menos 8 pedidos de ejemplo y usa Object.groupBy para agruparlos por estado. Muestra, para cada estado, cuántos pedidos hay y la suma de sus totales.

Ejercicio 6: Matriz y tres en raya

Escribe una función hayGanador(tablero) que reciba un tablero 3×3 como el de la sección 14 y devuelva 'X', 'O' o null. Debe comprobar filas, columnas y las dos diagonales. Pista: genera un array con las 8 líneas posibles y usa find con every.

Ejercicio 7: Desestructuración

Escribe una función separar(numeros) que devuelva [pares, impares] y úsala con desestructuración. Después, a partir de const ranking = ['Ana', 'Luis', 'Marta', 'Pedro'], extrae en una sola línea el primero, el tercero y el resto en un array.

Ejercicio 8: Informe de ventas encadenado

Dado un array de ventas { vendedor, producto, unidades, precioUnitario, mes } con al menos 10 registros, obtén con métodos encadenados: el importe total de cada venta, las ventas de un mes concreto, el ranking de vendedores por facturación (de mayor a menor) y el producto más vendido en unidades. Presenta el resultado con console.table.


Resumen

Concepto Sintaxis Modifica el original
Crear [], Array.of(x), Array.from(iterable, fn), new Array(n).fill(v) —
Acceder arr[i], arr.at(-1), arr.length —
Añadir/quitar al final push(x), pop() Sí
Añadir/quitar al principio unshift(x), shift() Sí
Insertar/borrar en medio splice(i, n, ...nuevos) Sí
Buscar valor includes(v), indexOf(v) No
Buscar por condición find, findIndex, findLast, findLastIndex No
Comprobar some(fn), every(fn) No
Recorrer for...of, entries(), forEach(fn) No
Transformar map(fn), filter(fn), reduce(fn, inicial), flat(n), flatMap(fn) No
Ordenar sort((a, b) => a - b), localeCompare, reverse() Sí
Inmutables (ES2023) toSorted, toReversed, toSpliced, with No
Extraer y unir slice(i, f), concat, [...a, ...b], join(sep) No
Desestructurar const [a, , c = 0, ...resto] = arr —
Agrupar (ES2024) Object.groupBy(arr, fn), Map.groupBy(arr, fn) No
Asíncrono (ES2026) await Array.fromAsync(iterable) —
Copiar [...arr] (superficial), structuredClone(arr) (profunda) —