Hasta ahora has trabajado sobre todo con valores sueltos: números, textos, booleanos. Los datos reales vienen agrupados (un usuario, un pedido, una lista de productos) y en JavaScript eso son objetos y arrays. Su forma de escribirse es muy cómoda, pero esconde una regla que hay que entender bien: las variables no contienen el objeto, sino una referencia a él.
1 // Objetos y arrays literales
En Java, para tener un objeto necesitas antes una clase. En JavaScript puedes escribir un objeto directamente, con pares clave-valor:
const alumno = {
nombre: 'Ana',
curso: '2DAW',
notas: [7, 9, 8],
activo: true,
};
alumno.nombre; // 'Ana' (notación de punto)
alumno['curso']; // '2DAW' (notación de corchetes)
alumno.email = 'ana@correo.es'; // añadir una propiedad nueva
delete alumno.activo; // eliminarlaLa notación de corchetes es necesaria cuando el nombre de la propiedad está en una variable: alumno[campo]. Si accedes a una propiedad que no existe no hay error; obtienes undefined. Para no fallar al leer dentro de algo que puede no existir está el encadenamiento opcional: alumno.direccion?.calle devuelve undefined en lugar de lanzar un error si direccion no existe.
Los arrays son objetos especiales con índices numéricos y una propiedad length. A diferencia de Java, no tienen tamaño fijo ni tipo único:
const mezcla = [1, 'dos', { tres: 3 }];
mezcla.push(4); // añade al final
mezcla.length; // 42 // Valores frente a referencias
Aquí está la diferencia importante. Con primitivos, asignar copia el valor:
let x = 10;
let y = x;
y = 20;
console.log(x); // 10: x no se ha visto afectadaCon objetos y arrays, asignar copia la referencia. Las dos variables apuntan al mismo objeto:
const a = { nombre: 'Ana' };
const b = a;
b.nombre = 'Luis';
console.log(a.nombre); // 'Luis'Si vienes de Java esto te sonará: es lo mismo que ocurre con cualquier objeto en Java. La diferencia es que en JavaScript los arrays y los objetos literales aparecen tan a menudo que el efecto se nota mucho más.
Esta regla tiene dos consecuencias directas:
- Comparar objetos compara identidades.
{ a: 1 } === { a: 1 }esfalse: son dos objetos distintos con el mismo contenido. - Una función puede modificar el objeto que recibe. Si pasas un array a una función y esta hace
push, el array de fuera cambia, porque es el mismo.
Una variable con un objeto guarda una flecha hacia el objeto, no el objeto. Asignar, pasar como argumento o guardar en otro array copia la flecha; el objeto sigue siendo uno solo.
3 // Desestructuración y spread
Antes de copiar objetos hacen falta dos herramientas que vas a ver en casi cualquier código moderno. Las dos se escriben con muy pocos caracteres, así que conviene reconocerlas a la primera.
La desestructuración extrae propiedades de un objeto o elementos de un array a variables sueltas, en una sola línea. A la izquierda del = se escribe la «forma» de lo que se quiere sacar: llaves para un objeto (por nombre de propiedad) y corchetes para un array (por posición):
const alumno = { nombre: 'Ana', curso: '2DAW', notas: [7, 9, 8] };
// Objetos: se extrae por nombre de propiedad
const { nombre, curso } = alumno; // nombre = 'Ana', curso = '2DAW'
const { email = 'sin email' } = alumno; // valor por defecto si no existe
const { nombre: nombreAlumno } = alumno; // renombrar al extraer
// Arrays: se extrae por posición
const [primera, segunda] = alumno.notas; // primera = 7, segunda = 9
const [, , tercera] = alumno.notas; // saltar posiciones: tercera = 8
const [cabeza, ...resto] = alumno.notas; // cabeza = 7, resto = [9, 8]Sin desestructuración, la primera línea serían dos: const nombre = alumno.nombre; y const curso = alumno.curso;. Se usa muchísimo en los parámetros de las funciones, porque deja claro qué propiedades usa una función del objeto que recibe:
function tarjeta({ nombre, curso }) {
return `${nombre} · ${curso}`;
}
tarjeta(alumno); // 'Ana · 2DAW'El operador spread (...) hace lo contrario: coge un array o un objeto y expande su contenido dentro de otro literal o de una llamada, como si hubieras escrito sus elementos uno a uno:
const notas = [7, 9, 8];
const masNotas = [...notas, 10]; // [7, 9, 8, 10]
const todas = [...notas, ...[5, 6]]; // [7, 9, 8, 5, 6]
Math.max(...notas); // 9: igual que Math.max(7, 9, 8)
const datos = { nombre: 'Ana', curso: '2DAW' };
const contacto = { email: 'ana@correo.es' };
const ficha = { ...datos, ...contacto }; // { nombre, curso, email }
const cambio = { ...datos, curso: '1DAW' }; // si se repite una propiedad, gana la últimaFíjate en que ... aparece en los dos bloques de código con papeles opuestos. En const [cabeza, ...resto] = notas agrupa lo que sobra en un array: se llama rest, y ya lo viste en los parámetros de las funciones. En [...notas, 10] expande: es el spread. Lo que decide cuál es cuál es dónde está escrito:
| Aspecto | Rest | Spread |
|---|---|---|
| Dónde aparece | A la izquierda del = o en los parámetros |
En un literal [] o {}, o en una llamada |
| Qué hace | Agrupa varios valores en un array u objeto | Expande un array u objeto en valores sueltos |
| Ejemplo | const [a, ...resto] = lista |
const copia = [...lista] |
4 // Copiar de verdad
Con el spread ya tienes la forma más habitual de crear un objeto independiente: un literal nuevo al que se le expande el contenido del original.
const original = { nombre: 'Ana', notas: [7, 9] };
const copia = { ...original }; // objeto nuevo
const conCambio = { ...original, nombre: 'Eva' }; // copia modificando una propiedad
const lista = [1, 2, 3];
const ampliada = [...lista, 4]; // array nuevo: [1, 2, 3, 4]El spread hace una copia superficial: crea nuevo el objeto exterior, pero cada propiedad se copia tal cual, y si una propiedad es un objeto o un array, lo que se copia es su referencia. Para copiar todos los niveles, lo que se llama una copia profunda, está structuredClone:
const superficial = { ...original };
superficial.notas.push(10);
console.log(original.notas); // [7, 9, 10]: el array es el mismo
const profunda = structuredClone(original);
profunda.notas.push(5);
console.log(original.notas); // [7, 9, 10]: la copia profunda no lo tocaQue la copia sea un objeto nuevo no significa que todo lo de dentro sea nuevo. Si el objeto tiene objetos o arrays dentro y los vas a cambiar, usa structuredClone(original) o copia también ese nivel con otro spread, como en el ejemplo siguiente.
Una función debe devolver un pedido con el estado cambiado a 'enviado', sin modificar el pedido original, que la aplicación usa en otra parte de la pantalla.
- Versión con fallo:
function enviar(pedido) { pedido.estado = 'enviado'; return pedido; }. Modifica el objeto recibido, que es el mismo que el original. - Crear un objeto nuevo:
return { ...pedido, estado: 'enviado' };. El pedido original conserva su estado. - Revisar lo anidado: si después se añaden líneas con
nuevo.lineas.push(...), el arraylineassigue compartido con el original. - Copiar también ese nivel:
return { ...pedido, estado: 'enviado', lineas: [...pedido.lineas] };.
5 // Para practicar
Crea objetos sin modificar los originales y comprueba en qué momento una copia deja de ser independiente. Cada ejercicio trae el enunciado en los comentarios del código.
Objeto persona y copias con spreadtraining-daw-sv/modulo-00/05-objetos-arrays-y-referencias/01_personDesestructuración y spreadtraining-daw-sv/modulo-00/05-objetos-arrays-y-referencias/02_destructuring_spreadAutoevaluación
Haces const b = a; siendo a un objeto, cambias b.nombre y también cambia a.nombre. ¿Por qué?
a y b apuntan al mismo objeto. Asignar un objeto no lo duplica: copia la referencia, es decir, la dirección donde está el objeto en memoria. Hay un solo objeto y dos nombres para llegar a él.¿Por qué [1, 2] === [1, 2] da false?
=== con objetos compara identidad, no contenido. Cada literal [1, 2] crea un array nuevo en memoria, así que son dos objetos distintos aunque tengan los mismos valores. Sólo da true si las dos referencias apuntan al mismo objeto.Copias un usuario con const copia = { ...usuario } y, al hacer copia.notas.push(5), el original también cambia. ¿Qué ha fallado?
structuredClone(usuario).