Módulo 00 // Unidad 05

Objetos, arrays y referencias

Cómo se guardan los datos compuestos y qué se copia realmente al asignarlos

Unidad para alumnos del curso

Comprobando tu sesión…

Inicia sesión con la cuenta de Google con la que estás matriculado para ver esta unidad.

Esa cuenta no tiene acceso a este curso. Prueba con otra.

No se ha podido cargar el inicio de sesión de Google. Comprueba la conexión o si algún bloqueador lo impide, y recarga la página.

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;   // eliminarla

La 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;          // 4

2 // 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 afectada

Con 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 } es false: 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 última

Fí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]
VARIABLES, OBJETOS Y REFERENCIAS variables objetos en memoria a b = a c = { ...a } objeto 1 nombre: 'Ana' notas: objeto 2 (copia) nombre: 'Ana' notas: [7, 9] misma referencia objeto nuevo array compartido: la copia es superficial
Sigue las flechas: b llega al mismo objeto que a, c llega a uno nuevo, pero las dos versiones del objeto terminan apuntando al mismo array de notas.

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 toca

Que 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.

  1. Versión con fallo: function enviar(pedido) { pedido.estado = 'enviado'; return pedido; }. Modifica el objeto recibido, que es el mismo que el original.
  2. Crear un objeto nuevo: return { ...pedido, estado: 'enviado' };. El pedido original conserva su estado.
  3. Revisar lo anidado: si después se añaden líneas con nuevo.lineas.push(...), el array lineas sigue compartido con el original.
  4. Copiar también ese nivel: return { ...pedido, estado: 'enviado', lineas: [...pedido.lineas] };.
La versión correcta crea un objeto nuevo en cada nivel que va a cambiar. Este estilo, en el que no se modifican los datos sino que se generan versiones nuevas, evita que una parte de la aplicación cambie datos que otra está usando sin saberlo.

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_spread

Autoevaluación

Haces const b = a; siendo a un objeto, cambias b.nombre y también cambia a.nombre. ¿Por qué?
Porque 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?
Porque === 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?
El spread hace una copia superficial. Crea un objeto nuevo, pero copia las propiedades tal cual: si una propiedad contiene un array, lo que se copia es la referencia a ese array. Para duplicar también lo anidado hace falta structuredClone(usuario).