Llevas usando arrays desde el Módulo 1, pero siempre como una lista de la que se sacan cosas por índice dentro de un for. Eso es aprovechar quizá el diez por ciento de lo que un array sabe hacer. En esta lección estudias la estructura a fondo: las cinco formas de crear uno, qué es exactamente length y qué ocurre si se lo cambias, por qué typeof [] === 'object' y qué usar en su lugar, y sobre todo la distinción que más errores evita en JavaScript real: métodos que modifican el array original frente a métodos que devuelven uno nuevo. Esa tabla que construirás en el apartado 7 es material de consulta permanente. Todo el recorrido se apoya en dos usos concretos del proyecto: mantener la lista de tareas del tablero de Taller Nómada y gestionar las etiquetas de una tarea, ese campo que el modelo canónico define como «array de strings» y que hasta ahora apenas has tocado.
Contenido
- Qué es un array y qué no
- Cinco formas de crear un array
- Índices,
at()y los índices negativos length: leerlo, y qué pasa al asignarlo- Huecos: los sparse arrays
Array.isArrayy por quétypeofno sirve- La tabla que hay que tener a mano: mutar frente a devolver
- Añadir y quitar por los extremos:
push,pop,shift,unshift splice: borrar, insertar y reemplazarslice: copiar un trozo sin tocar el originalconcatyjoin- Buscar:
indexOf,lastIndexOf,includesy su límite con objetos reversey una nota sobre las versiones inmutables- Arrays anidados,
flatyflatMap - Ejemplo integrado: las etiquetas de una tarea
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Qué es un array y qué no
Un array es una lista ordenada de valores, accesibles por su posición (índice), que empieza en 0.
const etiquetas = ['espacio', 'diseño', 'mobiliario'];
console.log(etiquetas[0]); // 'espacio'
console.log(etiquetas[2]); // 'mobiliario'
console.log(etiquetas[3]); // undefined ← no existe, pero no fallaTres propiedades definen a un array frente a un objeto normal:
| Objeto | Array | |
|---|---|---|
| Clave | Un nombre (titulo, estado) |
Una posición numérica (0, 1, 2…) |
| Orden | No garantizado para claves cualesquiera | Garantizado y significativo |
| Tamaño | Se cuenta con Object.keys(o).length |
array.length, mantenido por el motor |
Y una advertencia técnica que explica muchas rarezas: un array de JavaScript es, por debajo, un objeto con claves numéricas y una propiedad length especial. Por eso puedes hacer esto, aunque no debas:
const raro = ['a', 'b'];
raro.autor = 'Lucía'; // ✗ funciona, pero es una propiedad, no un elemento
console.log(raro.length); // 2 ← length solo cuenta los índices
console.log(raro); // [ 'a', 'b', autor: 'Lucía' ]Regla práctica: en un array van elementos, nunca propiedades con nombre. Si necesitas nombres, usa un objeto.
- Cinco formas de crear un array
// 1. Literal: la forma normal, la que usarás el 95 % de las veces
const etiquetas = ['espacio', 'diseño'];
const vacio = [];
// 2. new Array(): tiene una trampa famosa
const tres = new Array('a', 'b', 'c'); // [ 'a', 'b', 'c' ] ← varios argumentos: elementos
const seis = new Array(6); // [ <6 empty items> ] ← UN número: ¡longitud!
console.log(seis.length); // 6, pero está completamente vacío
// 3. Array.of(): resuelve esa trampa, siempre crea elementos
console.log(Array.of(6)); // [ 6 ] ← un array con el número 6
console.log(Array.of('a', 'b')); // [ 'a', 'b' ]
// 4. Array.from(): construye un array a partir de algo iterable o "parecido a un array"
console.log(Array.from('serigrafía')); // [ 's','e','r','i','g','r','a','f','í','a' ]
console.log(Array.from({ length: 4 }, (_, i) => i + 1)); // [ 1, 2, 3, 4 ]
// 5. Array(n).fill(): el idioma para crear n posiciones con el mismo valor
const horasPorDia = Array(5).fill(0);
console.log(horasPorDia); // [ 0, 0, 0, 0, 0 ]Merece la pena detenerse en dos:
new Array(6) frente a Array.of(6). Con un único argumento numérico, new Array interpreta que le estás pidiendo un array de esa longitud, vacío. Es una incoherencia histórica del lenguaje y la razón por la que Array.of existe. Evita new Array salvo para el caso deliberado de reservar longitud.
Array.from con función de mapeo. El segundo parámetro es una función que se aplica a cada posición, con el valor y el índice. Es la forma idiomática de generar secuencias:
// Los próximos 5 días laborables a partir de una fecha (versión simplificada con strings)
const diasDelMes = Array.from({ length: 5 }, (_, i) => `2026-09-${20 + i}`);
console.log(diasDelMes);
// [ '2026-09-20', '2026-09-21', '2026-09-22', '2026-09-23', '2026-09-24' ]El _ como nombre del primer parámetro es una convención: significa «este argumento existe pero no lo uso».
Cuidado con fill y los objetos, porque comparte la referencia (recuerda 01-05):
const filas = Array(3).fill({ horas: 0 });
filas[0].horas = 8;
console.log(filas); // [ { horas: 8 }, { horas: 8 }, { horas: 8 } ] ✗ ¡el mismo objeto!
const bien = Array.from({ length: 3 }, () => ({ horas: 0 }));
bien[0].horas = 8;
console.log(bien); // [ { horas: 8 }, { horas: 0 }, { horas: 0 } ] ✓fill copia el mismo valor tres veces; si ese valor es una referencia, las tres posiciones apuntan al mismo objeto. Array.from con función llama a la función tres veces y crea tres objetos distintos.
- Índices,
at() y los índices negativos
at() y los índices negativosEl acceso clásico con corchetes lo conoces. Lo que quizá no conozcas es at(), que hace lo mismo pero admite índices negativos contando desde el final:
const etiquetas = ['espacio', 'diseño', 'mobiliario', 'presupuesto'];
console.log(etiquetas[0]); // 'espacio'
console.log(etiquetas.at(0)); // 'espacio'
console.log(etiquetas.at(-1)); // 'presupuesto' ← el último
console.log(etiquetas.at(-2)); // 'mobiliario'
console.log(etiquetas[-1]); // undefined ✗ ¡ojo! busca una propiedad llamada "-1"Antes de at(), el último elemento se obtenía con etiquetas[etiquetas.length - 1], que funciona pero repite el nombre del array y es más ruidoso. Cuando el array es el resultado de una expresión larga, la diferencia se nota:
const ultimaTarea = tablero.tareas.at(-1);
// frente a
const ultimaTareaLarga = tablero.tareas[tablero.tareas.length - 1];
length: leerlo, y qué pasa al asignarlo
length: leerlo, y qué pasa al asignarlolength no es el número de elementos: es el índice más alto usado, más uno. Y, a diferencia de casi cualquier otro lenguaje, se puede escribir.
const etiquetas = ['espacio', 'diseño', 'mobiliario'];
console.log(etiquetas.length); // 3
// Asignar un length MENOR trunca el array (destruye elementos)
etiquetas.length = 2;
console.log(etiquetas); // [ 'espacio', 'diseño' ]
// Asignar un length MAYOR crea huecos
etiquetas.length = 5;
console.log(etiquetas); // [ 'espacio', 'diseño', <3 empty items> ]
// El idioma para vaciar un array sin cambiar la referencia
etiquetas.length = 0;
console.log(etiquetas); // []Esa última línea es un truco útil: etiquetas = [] crearía un array nuevo (y con const fallaría), mientras que etiquetas.length = 0 vacía el que ya existe, de modo que cualquier otra variable que lo estuviera apuntando también verá el array vacío. Es exactamente la diferencia entre reasignar y mutar que estudiaste con las referencias.
Y ojo con asignar un índice muy alto:
const tareas = ['a', 'b'];
tareas[9] = 'j';
console.log(tareas.length); // 10
console.log(tareas); // [ 'a', 'b', <7 empty items>, 'j' ]
- Huecos: los sparse arrays
Un array con posiciones que nunca se han asignado se llama sparse (disperso o con huecos). Los huecos no son undefined: son la ausencia de la propiedad, y algunos métodos los saltan.
const conHueco = [1, , 3]; // la coma doble deja un hueco en la posición 1
console.log(conHueco.length); // 3
console.log(conHueco[1]); // undefined ← al leerlo parece undefined
console.log(1 in conHueco); // false ← pero la clave no existe
console.log(Object.keys(conHueco)); // [ '0', '2' ]
const conUndefined = [1, undefined, 3];
console.log(1 in conUndefined); // true ← aquí la clave sí existeEn la práctica: evita los huecos. Aparecen casi siempre por accidente (delete tareas[2], que borra el elemento pero deja el hueco, o new Array(6)), y hacen que unos métodos los ignoren y otros no, lo que produce bugs difíciles de ver. Si quieres eliminar un elemento de verdad, usa splice (apartado 9), no delete:
const tareas = ['a', 'b', 'c'];
delete tareas[1];
console.log(tareas); // [ 'a', <1 empty item>, 'c' ] ✗ length sigue siendo 3
const tareas2 = ['a', 'b', 'c'];
tareas2.splice(1, 1);
console.log(tareas2); // [ 'a', 'c' ] ✓ length ahora es 2
Array.isArray y por qué typeof no sirve
Array.isArray y por qué typeof no sirveconsole.log(typeof []); // 'object' ✗ inútil
console.log(typeof {}); // 'object'
console.log(typeof null); // 'object' (el bug histórico de 01-05)
console.log(Array.isArray([])); // true ✓
console.log(Array.isArray({})); // false
console.log(Array.isArray('espacio')); // falsetypeof no distingue arrays de objetos porque, como vimos, un array es un objeto. Array.isArray es la comprobación correcta, y es especialmente útil validando datos del modelo, donde etiquetas debe ser siempre un array:
function validarEtiquetas(tarea) {
if (!Array.isArray(tarea.etiquetas)) {
throw new Error(`Las etiquetas de la tarea ${tarea.id} deben ser un array.`);
}
return true;
}
validarEtiquetas({ id: 1, etiquetas: ['espacio'] }); // true
// validarEtiquetas({ id: 8, etiquetas: 'espacio' }); // ✗ Error
- La tabla que hay que tener a mano: mutar frente a devolver
Esta es la sección más importante de la lección. Los métodos de array se dividen en dos familias, y confundirlas es la causa número uno de bugs con listas:
- Los que mutan: modifican el array sobre el que se llaman. Suelen devolver otra cosa (el nuevo
length, el elemento eliminado…) o el propio array. - Los que devuelven uno nuevo: dejan el original intacto y producen un array (o un valor) distinto.
| Método | ¿Muta el original? | Qué devuelve |
|---|---|---|
push(...v) |
Sí | El nuevo length |
pop() |
Sí | El elemento eliminado |
shift() |
Sí | El primer elemento, eliminado |
unshift(...v) |
Sí | El nuevo length |
splice(i, n, ...v) |
Sí | Un array con los elementos eliminados |
reverse() |
Sí | El mismo array, ya invertido |
sort(cmp) |
Sí (lección 04-05) | El mismo array, ya ordenado |
fill(v) |
Sí | El mismo array |
slice(i, j) |
No | Un array nuevo con el trozo |
concat(...) |
No | Un array nuevo con todo unido |
join(sep) |
No | Un string |
indexOf / lastIndexOf / includes |
No | Un número / un booleano |
at(i) |
No | El elemento |
flat() / flatMap(f) |
No | Un array nuevo |
map / filter (04-04, 04-05) |
No | Un array nuevo |
toSorted / toReversed (04-05) |
No | Un array nuevo |
Una regla mnemotécnica bastante fiable: los métodos que mutan tienen nombres de verbo de acción sobre la lista (empujar, sacar, empalmar, invertir, ordenar, rellenar); los que devuelven algo nuevo tienen nombres de producir (rebanar, concatenar, unir, aplanar, mapear). Y una regla de estilo: en Nómada Tareas preferimos los métodos que no mutan siempre que sea posible, porque un array que nadie modifica por sorpresa es un array del que te puedes fiar. En 04-07 verás cómo construir actualizaciones inmutables completas.
flowchart TD
A["const original = ['a','b','c']"] --> B{"¿Qué método?"}
B -->|"original.push('d')"| C["original = ['a','b','c','d']<br/>devuelve 4"]
B -->|"original.slice(0, 2)"| D["original SIGUE ['a','b','c']<br/>devuelve ['a','b']"]
- Añadir y quitar por los extremos:
push, pop, shift, unshift
push, pop, shift, unshiftLos cuatro mutan. Se distinguen por el extremo y la dirección:
| Método | Extremo | Acción | Devuelve |
|---|---|---|---|
push(v) |
Final | Añade | Nuevo length |
pop() |
Final | Quita | El elemento quitado |
unshift(v) |
Principio | Añade | Nuevo length |
shift() |
Principio | Quita | El elemento quitado |
const etiquetas = ['espacio', 'diseño'];
console.log(etiquetas.push('mobiliario')); // 3 ← devuelve el length, no el array
console.log(etiquetas); // [ 'espacio', 'diseño', 'mobiliario' ]
console.log(etiquetas.push('presupuesto', 'obra')); // 5 ← admite varios de golpe
const ultima = etiquetas.pop();
console.log(ultima); // 'obra'
console.log(etiquetas.length); // 4
etiquetas.unshift('urgente');
console.log(etiquetas);
// [ 'urgente', 'espacio', 'diseño', 'mobiliario', 'presupuesto' ]
const primera = etiquetas.shift();
console.log(primera); // 'urgente'Un aviso de rendimiento que importará en el Módulo 9: push y pop trabajan en el final y son muy rápidos. shift y unshift trabajan en el principio y obligan al motor a reindexar todos los elementos, así que en listas grandes son notablemente más costosos. Con las seis tareas de Taller Nómada da igual; con cien mil registros, no.
Aplicado al tablero:
const tablero = { tareas: [] };
function agregarTarea(tablero, tarea) {
tablero.tareas.push(tarea);
return tablero.tareas.length;
}
console.log(agregarTarea(tablero, { id: 1, titulo: 'Rediseñar la sala polivalente' })); // 1
console.log(agregarTarea(tablero, { id: 2, titulo: 'Cartelería del taller de serigrafía' })); // 2
splice: borrar, insertar y reemplazar
splice: borrar, insertar y reemplazarsplice es el método más versátil y el más fácil de escribir mal. Su firma es:
Sus tres usos, con el mismo array de partida:
// USO 1 — BORRAR: segundo argumento > 0, sin elementos que insertar
const a = ['espacio', 'diseño', 'mobiliario', 'presupuesto'];
const borrados = a.splice(1, 2); // desde el índice 1, borra 2
console.log(borrados); // [ 'diseño', 'mobiliario' ] ← lo eliminado
console.log(a); // [ 'espacio', 'presupuesto' ]
// USO 2 — INSERTAR: segundo argumento 0, más los elementos nuevos
const b = ['espacio', 'presupuesto'];
b.splice(1, 0, 'diseño', 'mobiliario'); // en el índice 1, borra 0, inserta 2
console.log(b); // [ 'espacio', 'diseño', 'mobiliario', 'presupuesto' ]
// USO 3 — REEMPLAZAR: borrar e insertar a la vez
const c = ['espacio', 'diseño', 'mobiliario'];
c.splice(1, 1, 'interiorismo'); // en el índice 1, borra 1, inserta 1
console.log(c); // [ 'espacio', 'interiorismo', 'mobiliario' ]Detalles que evitan sorpresas:
- Devuelve lo eliminado, no el array resultante. Si escribes
const nuevo = a.splice(1, 2),nuevocontiene los elementos borrados. El array modificado sigue siendoa. - Si omites el segundo argumento, borra hasta el final:
a.splice(2)elimina todo desde el índice 2. - Admite índices negativos:
a.splice(-1, 1)borra el último elemento.
Aplicado al proyecto, eliminar una tarea del tablero por su id:
function eliminarTarea(tablero, id) {
for (let i = 0; i < tablero.tareas.length; i++) {
if (tablero.tareas[i].id === id) {
const [eliminada] = tablero.tareas.splice(i, 1);
return eliminada;
}
}
return null;
}(La línea const [eliminada] = ... es desestructuración de arrays: coge el primer elemento del array que devuelve splice. Es el tema de 04-06; por ahora léela como «el primero de lo que se ha borrado».)
slice: copiar un trozo sin tocar el original
slice: copiar un trozo sin tocar el originalslice(inicio, fin) devuelve un array nuevo con los elementos desde inicio hasta fin, sin incluir fin. No toca el original.
const tareas = ['T1', 'T2', 'T3', 'T4', 'T5', 'T6'];
console.log(tareas.slice(0, 3)); // [ 'T1', 'T2', 'T3' ] ← el 3 NO entra
console.log(tareas.slice(3)); // [ 'T4', 'T5', 'T6' ] ← desde el 3 al final
console.log(tareas.slice(-2)); // [ 'T5', 'T6' ] ← los dos últimos
console.log(tareas.slice()); // copia completa
console.log(tareas); // intacto ✓slice() sin argumentos es el idioma clásico para copiar un array, y es lo que permite ordenar sin destruir el original:
const copia = tareas.slice();
copia.reverse(); // reverse muta... pero solo la copia
console.log(tareas[0]); // 'T1' ← el original está a salvoDistinguir slice de splice es imprescindible. Los nombres se parecen; el comportamiento es opuesto:
slice(i, j) |
splice(i, n, ...) |
|
|---|---|---|
| ¿Muta? | No | Sí |
| Segundo argumento | Índice final (excluido) | Cantidad a borrar |
| Devuelve | El trozo copiado | Lo eliminado |
| Sirve para | Copiar, paginar | Borrar, insertar, reemplazar |
Truco mnemotécnico: splice tiene una p de «peligro» (muta).
concat y join
concat y joinconcat une arrays y devuelve uno nuevo:
const deIvan = ['Rediseñar la sala polivalente', 'Guía de encuadernación para residentes'];
const deMarta = ['Cartelería del taller de serigrafía'];
const todas = deIvan.concat(deMarta);
console.log(todas.length); // 3
console.log(deIvan.length); // 2 ← intacto
// Admite varios arrays y también valores sueltos
console.log(deIvan.concat(deMarta, ['Presupuesto de la carpintería'], 'Extra').length); // 5En código moderno, el operador spread de 04-07 sustituye a concat casi siempre ([...deIvan, ...deMarta]), pero concat sigue siendo perfectamente válido y aparece en mucho código existente.
join convierte un array en un string, uniendo los elementos con el separador que le indiques (por defecto, una coma):
const etiquetas = ['carpintería', 'compras'];
console.log(etiquetas.join()); // 'carpintería,compras'
console.log(etiquetas.join(', ')); // 'carpintería, compras'
console.log(etiquetas.join(' · ')); // 'carpintería · compras'
console.log([].join(', ')); // '' ← array vacío: string vacíoEs exactamente lo que necesitas para pintar las etiquetas de una tarea en una línea de informe:
function lineaConEtiquetas(tarea) {
const etiquetas = tarea.etiquetas.length > 0
? ` #${tarea.etiquetas.join(' #')}`
: '';
return `[${tarea.id}] ${tarea.titulo}${etiquetas}`;
}
console.log(lineaConEtiquetas({ id: 6, titulo: 'Presupuesto de la carpintería',
etiquetas: ['carpintería', 'compras'] }));
// [6] Presupuesto de la carpintería #carpintería #comprasCuidado con null y undefined dentro del array: join los convierte en string vacío, no en 'null'.
- Buscar:
indexOf, lastIndexOf, includes y su límite con objetos
indexOf, lastIndexOf, includes y su límite con objetosconst etiquetas = ['serigrafía', 'compras', 'serigrafía', 'almacén'];
console.log(etiquetas.indexOf('compras')); // 1
console.log(etiquetas.indexOf('serigrafía')); // 0 ← la PRIMERA aparición
console.log(etiquetas.lastIndexOf('serigrafía')); // 2 ← la ÚLTIMA
console.log(etiquetas.indexOf('web')); // -1 ← no está
console.log(etiquetas.includes('almacén')); // true
console.log(etiquetas.includes('web')); // falseindexOf devuelve -1 cuando no encuentra nada, lo que obliga a escribir comprobaciones incómodas. includes (ES2016) devuelve directamente un booleano y es lo que debes usar cuando solo te interesa saber si está:
// Antiguo, todavía muy visto
if (etiquetas.indexOf('compras') !== -1) { }
// Moderno y legible
if (etiquetas.includes('compras')) { }Ahora, el límite importante. Los dos comparan con ===, así que funcionan con primitivos pero no con objetos, porque dos objetos distintos nunca son iguales aunque tengan el mismo contenido (01-07):
const tareas = [
{ id: 1, titulo: 'Rediseñar la sala polivalente' },
{ id: 6, titulo: 'Presupuesto de la carpintería' }
];
console.log(tareas.includes({ id: 6, titulo: 'Presupuesto de la carpintería' })); // false ✗
// Con la MISMA referencia sí funciona
const referencia = tareas[1];
console.log(tareas.includes(referencia)); // true ✓
console.log(tareas.indexOf(referencia)); // 1Para buscar objetos por contenido necesitas find y findIndex, que reciben una función de comparación. Son el arranque de la lección Buscar, Ordenar y Agregar Datos.
Una última rareza que conviene conocer: includes sí encuentra NaN, y indexOf no.
console.log([NaN].includes(NaN)); // true
console.log([NaN].indexOf(NaN)); // -1 ← porque NaN !== NaN (01-07)
reverse y una nota sobre las versiones inmutables
reverse y una nota sobre las versiones inmutablesreverse() invierte el array en el sitio: muta.
const orden = ['pendiente', 'en-curso', 'hecha'];
const devuelto = orden.reverse();
console.log(orden); // [ 'hecha', 'en-curso', 'pendiente' ] ← ¡cambiado!
console.log(devuelto === orden); // true ← devuelve el MISMO array, no una copiaEse true es la trampa: como reverse devuelve el array, parece que estás creando uno nuevo, y no. Si necesitas conservar el original, copia primero:
const orden2 = ['pendiente', 'en-curso', 'hecha'];
const alReves = orden2.slice().reverse();
console.log(orden2); // [ 'pendiente', 'en-curso', 'hecha' ] ✓ intacto
console.log(alReves); // [ 'hecha', 'en-curso', 'pendiente' ]JavaScript moderno incorpora toReversed(), que hace justo eso sin la copia manual. Lo verás junto a toSorted() en 04-05, donde el problema de la mutación se vuelve realmente sangrante.
- Arrays anidados,
flat y flatMap
flat y flatMapUn array puede contener arrays. Es lo que ocurre, por ejemplo, si recoges las etiquetas de todas las tareas:
const etiquetasPorTarea = [
['espacio', 'diseño'],
['serigrafía', 'comunicación'],
['web', 'reservas']
];
console.log(etiquetasPorTarea[1][0]); // 'serigrafía'
console.log(etiquetasPorTarea.length); // 3 ← tres arrays, no seis stringsflat() aplana un nivel; con un argumento, aplana los que le digas:
console.log(etiquetasPorTarea.flat());
// [ 'espacio', 'diseño', 'serigrafía', 'comunicación', 'web', 'reservas' ]
const profundo = [1, [2, [3, [4]]]];
console.log(profundo.flat()); // [ 1, 2, [ 3, [ 4 ] ] ] ← un solo nivel
console.log(profundo.flat(2)); // [ 1, 2, 3, [ 4 ] ]
console.log(profundo.flat(Infinity)); // [ 1, 2, 3, 4 ] ← todos los nivelesflatMap(f) es transformar y aplanar un nivel de una sola pasada. Es el atajo perfecto para «una lista con todas las etiquetas del backlog»:
const backlog = [
{ id: 1, etiquetas: ['espacio', 'diseño'] },
{ id: 2, etiquetas: ['serigrafía', 'comunicación'] },
{ id: 4, etiquetas: [] }
];
console.log(backlog.flatMap((t) => t.etiquetas));
// [ 'espacio', 'diseño', 'serigrafía', 'comunicación' ]Fíjate en que la tarea sin etiquetas simplemente no aporta nada: flatMap con un array vacío es también la forma idiomática de «transformar y descartar a la vez». Los detalles de map como transformación llegan en la lección siguiente.
- Ejemplo integrado: las etiquetas de una tarea
Juntemos todo en un pequeño juego de funciones para gestionar el campo etiquetas, respetando la regla R9 del proyecto (minúsculas y sin duplicados). Todas ellas devuelven un array nuevo: no mutan la tarea.
'use strict';
function normalizar(etiqueta) {
return etiqueta.trim().toLowerCase();
}
/** Añade una etiqueta si no estaba. Devuelve un array NUEVO. */
function conEtiqueta(etiquetas, nueva) {
const limpia = normalizar(nueva);
if (limpia === '') return etiquetas.slice();
if (etiquetas.includes(limpia)) return etiquetas.slice();
return etiquetas.concat(limpia);
}
/** Quita una etiqueta si estaba. Devuelve un array NUEVO. */
function sinEtiqueta(etiquetas, quitar) {
const limpia = normalizar(quitar);
const copia = etiquetas.slice();
const posicion = copia.indexOf(limpia);
if (posicion !== -1) copia.splice(posicion, 1);
return copia;
}
/** Reemplaza una etiqueta por otra, conservando la posición. */
function renombrarEtiqueta(etiquetas, antigua, nueva) {
const copia = etiquetas.slice();
const posicion = copia.indexOf(normalizar(antigua));
if (posicion !== -1) copia.splice(posicion, 1, normalizar(nueva));
return copia;
}
const originales = ['carpintería', 'compras'];
console.log(conEtiqueta(originales, ' URGENTE ')); // [ 'carpintería', 'compras', 'urgente' ]
console.log(conEtiqueta(originales, 'Compras')); // [ 'carpintería', 'compras' ] ← ya estaba
console.log(sinEtiqueta(originales, 'compras')); // [ 'carpintería' ]
console.log(renombrarEtiqueta(originales, 'compras', 'Presupuestos'));
// [ 'carpintería', 'presupuestos' ]
console.log(originales); // [ 'carpintería', 'compras' ] ✓ intacto en las cuatro llamadasEsa última línea es el objetivo de todo el diseño: el array de partida nunca cambia. Cada función devuelve una versión nueva, exactamente igual que pesoDePrioridad devolvía un número sin tocar nada. Es la misma idea de «función pura» de 03-03, ahora aplicada a listas.
Y aquí una comparación entre las dos estrategias, para que veas el precio de cada una:
// MUTANTE: rápida, pero cualquiera que tuviera el array ve el cambio
function agregarEtiquetaMutando(tarea, nueva) {
tarea.etiquetas.push(normalizar(nueva));
}
// INMUTABLE: crea una copia, pero nadie se lleva sorpresas
function agregarEtiquetaSinMutar(tarea, nueva) {
return { ...tarea, etiquetas: conEtiqueta(tarea.etiquetas, nueva) };
}(El { ...tarea, ... } de la segunda es el spread de objetos, que estudiarás en 04-07. Anótalo mentalmente: es la pieza que faltará para completar el patrón.)
Errores Comunes y Consejos
1. Esperar que push devuelva el array. Devuelve el nuevo length. const lista = tareas.push(x) deja lista valiendo un número.
2. Confundir slice con splice. slice copia y no muta; splice corta y muta. Si el resultado sale raro, comprueba cuál has escrito.
3. Usar delete para eliminar un elemento. Deja un hueco y no cambia length. Usa splice(i, 1).
4. Buscar objetos con includes o indexOf. Comparan por referencia, no por contenido. Usa find/findIndex (04-05).
5. Modificar un array mientras lo recorres con índices. Si haces splice dentro de un for ascendente, los índices se desplazan y te saltas elementos. Dos soluciones: recorrer hacia atrás, o construir un array nuevo con filter.
const tareas = ['a', 'b', 'b', 'c'];
// ✗ Se salta la segunda 'b'
for (let i = 0; i < tareas.length; i++) {
if (tareas[i] === 'b') tareas.splice(i, 1);
}
console.log(tareas); // [ 'a', 'b', 'c' ]
// ✓ Hacia atrás, los índices pendientes no se mueven
const tareas2 = ['a', 'b', 'b', 'c'];
for (let i = tareas2.length - 1; i >= 0; i--) {
if (tareas2[i] === 'b') tareas2.splice(i, 1);
}
console.log(tareas2); // [ 'a', 'c' ]6. new Array(3) cuando querías [3]. Usa Array.of(3) o directamente el literal.
7. Guardar propiedades con nombre en un array. tareas.total = 6 es legal y es casi siempre un error de diseño: usa un objeto { tareas: [...], total: 6 }.
Consejo profesional. Ante cualquier método de array que no recuerdes, hazte una sola pregunta antes de escribirlo: ¿muta o devuelve? Si muta y no querías mutar, antepón .slice(). Esa costumbre —copiar antes de modificar— te ahorrará más horas de depuración que cualquier otro hábito de este módulo.
Ejercicios
Ejercicio 1 — Cola del taller. El taller de serigrafía atiende encargos por orden de llegada. Partiendo de const cola = ['Cartelería del taller', 'Camisetas del curso'];:
- Añade
'Bolsas de tela'al final y muestra cuántos hay en cola. - Atiende el primer encargo (sácalo del principio) y muéstralo.
- Añade
'Reimpresión urgente'al principio, porque Marta la ha priorizado. - Muestra el último encargo de la cola sin usar
length. - Muestra la cola como un string separado por
' → '.
Ejercicio 2 — Reordenar la lista de tareas. Escribe una función moverTarea(tareas, desde, hasta) que devuelva un array nuevo con la tarea de la posición desde colocada en la posición hasta, sin mutar el original. Pruébala moviendo la tarea del índice 0 al índice 2 en ['T1','T2','T3','T4'], y comprueba que el array original no ha cambiado.
Ejercicio 3 — Auditoría de etiquetas. Dado el backlog canónico (con sus campos id y etiquetas), escribe:
todasLasEtiquetas(backlog), que devuelva un array plano con todas las etiquetas, incluidas las repetidas.tareasConEtiqueta(backlog, etiqueta), que devuelva un array con losidde las tareas que llevan esa etiqueta.etiquetasValidas(backlog), que devuelvatruesolo si todas las tareas tienenetiquetascomo array de verdad (usaArray.isArray).
Soluciones
Ejercicio 1
const cola = ['Cartelería del taller', 'Camisetas del curso'];
// 1. push devuelve el nuevo length
console.log(cola.push('Bolsas de tela')); // 3
// 2. shift saca del principio y devuelve el elemento
const atendido = cola.shift();
console.log(atendido); // 'Cartelería del taller'
// 3. unshift añade al principio
cola.unshift('Reimpresión urgente');
console.log(cola);
// [ 'Reimpresión urgente', 'Camisetas del curso', 'Bolsas de tela' ]
// 4. at(-1) sin recurrir a length
console.log(cola.at(-1)); // 'Bolsas de tela'
// 5. join con separador
console.log(cola.join(' → '));
// Reimpresión urgente → Camisetas del curso → Bolsas de telaUna cola real (primero en entrar, primero en salir) se implementa así: push para entrar y shift para salir. Si en vez de una cola quisieras una pila (último en entrar, primero en salir), serían push y pop.
Ejercicio 2
function moverTarea(tareas, desde, hasta) {
if (desde < 0 || desde >= tareas.length) {
throw new Error(`Posición de origen no válida: ${desde}`);
}
const copia = tareas.slice(); // 1. copiar para no mutar
const [movida] = copia.splice(desde, 1); // 2. sacarla de su sitio
copia.splice(hasta, 0, movida); // 3. insertarla en el destino
return copia;
}
const original = ['T1', 'T2', 'T3', 'T4'];
const movido = moverTarea(original, 0, 2);
console.log(movido); // [ 'T2', 'T3', 'T1', 'T4' ]
console.log(original); // [ 'T1', 'T2', 'T3', 'T4' ] ✓ intactoLas tres líneas del cuerpo resumen la lección entera: copiar con slice, quitar con splice(i, 1) aprovechando que devuelve lo eliminado, e insertar con splice(i, 0, v). Un error habitual es olvidar el slice inicial: la función seguiría funcionando, pero mutaría la lista de quien la llama, que es justo lo que queríamos evitar.
Ejercicio 3
const backlog = [
{ id: 1, etiquetas: ['espacio', 'diseño'] },
{ id: 2, etiquetas: ['serigrafía', 'comunicación'] },
{ id: 3, etiquetas: ['web', 'reservas'] },
{ id: 4, etiquetas: ['serigrafía', 'almacén'] },
{ id: 5, etiquetas: ['encuadernación', 'documentación'] },
{ id: 6, etiquetas: ['carpintería', 'compras'] }
];
function todasLasEtiquetas(backlog) {
return backlog.flatMap((tarea) => tarea.etiquetas);
}
function tareasConEtiqueta(backlog, etiqueta) {
const ids = [];
for (const tarea of backlog) {
if (tarea.etiquetas.includes(etiqueta)) ids.push(tarea.id);
}
return ids;
}
function etiquetasValidas(backlog) {
for (const tarea of backlog) {
if (!Array.isArray(tarea.etiquetas)) return false;
}
return true;
}
console.log(todasLasEtiquetas(backlog).length); // 12
console.log(tareasConEtiqueta(backlog, 'serigrafía')); // [ 2, 4 ]
console.log(tareasConEtiqueta(backlog, 'web')); // [ 3 ]
console.log(etiquetasValidas(backlog)); // true
console.log(etiquetasValidas([{ id: 9, etiquetas: 'web' }])); // falseDoce etiquetas en total, pero solo once distintas: 'serigrafía' aparece en las tareas 2 y 4. Deduplicarlas con un Set es uno de los ejemplos de 04-05. Y fíjate en etiquetasValidas: devuelve false en cuanto encuentra un fallo, sin recorrer el resto. Es el patrón de salida temprana de 02-04, y es exactamente lo que hace el método every que estudiarás en la lección 04-05.
Conclusión
Ahora entiendes el array como estructura, no solo como «lo que se recorre con un for». Sabes crearlo de cinco maneras y por qué new Array(6) no es lo mismo que Array.of(6); sabes que length es escribible y que asignarle 0 es el idioma para vaciar una lista sin cambiar su referencia; sabes qué son los huecos y por qué conviene evitarlos; y sabes que typeof [] no sirve de nada, mientras que Array.isArray responde exactamente lo que preguntas.
Sobre todo, tienes la tabla de mutar frente a devolver, que es la brújula de todo trabajo con listas: push, pop, shift, unshift, splice, reverse, sort y fill modifican el original; slice, concat, join, at, flat, flatMap, includes e indexOf no. Has visto splice en sus tres usos —borrar, insertar y reemplazar—, has distinguido slice de splice, has aprendido que includes e indexOf comparan con === y por eso fallan con objetos, y has aplicado todo ello a las etiquetas del proyecto con un juego de funciones que nunca mutan la tarea original, más las funciones moverTarea y eliminarTarea del tablero.
Queda una cosa por resolver, y es precisamente la que se repite en casi todo el código de esta lección: los bucles escritos a mano. tareasConEtiqueta recorre con for...of y va empujando en un array auxiliar; etiquetasValidas recorre para salir en cuanto encuentra un fallo; eliminarTarea recorre con índices para localizar una posición. Recorrer una lista es tan frecuente que JavaScript ofrece seis formas distintas de hacerlo, cada una con sus reglas sobre el índice, sobre si se puede interrumpir y sobre qué devuelve. Cuál elegir en cada caso es el tema de Iteración sobre Arrays, donde además volverás a encontrarte con map, esta vez con el método nativo y no con la versión casera que escribiste en 03-06.
Curso de JavaScript: De Principiante a Avanzado
Módulo 1: Introducción a JavaScript
- ¿Qué es JavaScript?
- Configuración de tu Entorno de Desarrollo
- Tu Primer Programa en JavaScript
- Sintaxis y Conceptos Básicos de JavaScript
- Variables y Tipos de Datos
- Operadores Básicos
- Conversión de Tipos y Comparaciones
- El Proyecto del Curso: Nómada Tareas
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for, while, do-while
- Sentencias Switch
- Control del Flujo: break, continue y Bucles Anidados
- Manejo de Errores con try-catch
Módulo 3: Funciones
- Definición y Llamada de Funciones
- Expresiones de Función y Funciones Flecha
- Parámetros y Valores de Retorno
- Ámbito y Closures
- Hoisting y el Contexto de Ejecución
- Funciones de Orden Superior
- Recursividad
Módulo 4: Objetos y Arrays
- Introducción a los Objetos
- Métodos de Objeto y la Palabra Clave
this - Arrays: Conceptos Básicos y Métodos
- Iteración sobre Arrays
- Buscar, Ordenar y Agregar Datos: find, sort y reduce
- Desestructuración de Arrays
- Desestructuración de Objetos, Spread y Rest
- JSON y Copias de Objetos
Módulo 5: Objetos y Funciones Avanzadas
- Prototipos y Herencia
- Clases y Programación Orientada a Objetos
- Encapsulación: Getters, Setters y Campos Privados
- Módulos e Importación/Exportación
- JavaScript Asíncrono: Callbacks
- Promesas y Async/Await
- El Bucle de Eventos y la Cola de Microtareas
- Iteradores y Generadores
Módulo 6: El Modelo de Objetos del Documento (DOM)
- Introducción al DOM
- Selección y Manipulación de Elementos del DOM
- Manejo de Eventos
- Propagación, Delegación y Eventos Personalizados
- Creación y Eliminación de Elementos del DOM
- Renderizado de Listas y Plantillas HTML
- Manejo y Validación de Formularios
Módulo 7: APIs del Navegador y Temas Avanzados
- Almacenamiento Local y de Sesión
- Fetch API y AJAX
- Peticiones Robustas: Errores, Timeouts y AbortController
- WebSockets
- Service Workers y Aplicaciones Web Progresivas (PWAs)
- APIs del Navegador Esenciales
- Introducción a WebAssembly
Módulo 8: Pruebas y Depuración
- Depuración de JavaScript
- Calidad de Código: ESLint, Prettier y Convenciones
- Pruebas Unitarias con Jest
- Dobles de Prueba: Mocks, Stubs y Spies
- Pruebas de Integración
- Pruebas de Extremo a Extremo con Cypress
Módulo 9: Rendimiento y Optimización
- Medir Antes de Optimizar: DevTools y Web Vitals
- Optimización del Rendimiento de JavaScript
- Gestión de Memoria
- Manipulación Eficiente del DOM
- Carga Perezosa y División de Código
Módulo 10: Frameworks y Librerías de JavaScript
- Por Qué Existen los Frameworks
- Introducción a React
- Gestión de Estado con Redux
- Conceptos Básicos de Vue.js
- Conceptos Básicos de Angular
- Elegir el Framework Adecuado
