El espejismo de la copia superficial
Cuando copias un objeto con el spread ({ ...obj }) o con
Object.assign({}, obj), JavaScript copia solo el primer nivel. Si alguna
propiedad es a su vez un objeto o un array, lo que se copia es la referencia,
no su contenido. La copia y el original comparten ese objeto anidado.
Piensa en una fotocopia de un álbum de fotos: la fotocopia tiene su propia portada, pero en lugar de copiar las fotos pega un post-it que dice "las fotos están en el álbum original". Si alguien tacha una foto, aparece tachada en ambos.
const original = {
nombre: "Ana",
direccion: { ciudad: "Madrid" },
};
const copia = { ...original };
copia.nombre = "Bea"; // afecta SOLO a la copia (primer nivel)
copia.direccion.ciudad = "Bilbao"; // ¡afecta TAMBIÉN al original!
console.log(original.nombre); // "Ana" (bien)
console.log(original.direccion.ciudad); // "Bilbao" (¡bug!)
El nombre es un string (primitivo) y se copió de verdad. Pero
direccion es un objeto: ambos lo comparten. Cambiar uno cambia el otro.
Copia profunda (deep copy)
Una copia profunda clona recursivamente todos los niveles, de modo que la copia es totalmente independiente del original. Hay tres caminos habituales:
1. structuredClone (la forma moderna)
Es una función nativa del navegador y de Node moderno que clona en profundidad:
const copia = structuredClone(original);
copia.direccion.ciudad = "Bilbao";
console.log(original.direccion.ciudad); // "Madrid" (intacto)
Soporta objetos, arrays, Map, Set, fechas... pero no funciones.
2. JSON (para datos simples)
Convertir a texto y volver crea una copia profunda en una línea:
const copia = JSON.parse(JSON.stringify(original));
Es práctico, pero solo sirve para datos "JSON-puros": pierde funciones,
undefined, fechas (se vuelven string), Map/Set, etc.
3. Recursión a mano
Recorrer la estructura y clonar cada nivel. Es lo que hacen las dos opciones anteriores por dentro:
function clonar(valor) {
if (Array.isArray(valor)) return valor.map(clonar);
if (valor && typeof valor === "object") {
const salida = {};
for (const clave of Object.keys(valor)) salida[clave] = clonar(valor[clave]);
return salida;
}
return valor; // primitivos: se devuelven tal cual
}
Congelar para impedir mutaciones: Object.freeze
Si quieres garantizar que un objeto no se modifique, Object.freeze lo
vuelve de solo lectura: cualquier intento de cambio se ignora (o lanza error en
modo estricto).
const config = Object.freeze({ tema: "oscuro" });
config.tema = "claro"; // ignorado
console.log(config.tema); // "oscuro"
Cuidado:
Object.freezetambién es superficial. Congela el primer nivel, pero los objetos anidados siguen siendo mutables salvo que los congeles tú también (un "freeze profundo" recursivo).
Ejemplos
El bug de la copia superficial
const original = { nivel: 1, datos: { puntos: [10, 20] } };
const copia = { ...original };
copia.datos.puntos.push(30); // muta el array COMPARTIDO
console.log("original:", original.datos.puntos); // [10, 20, 30] ¡contaminado!
console.log("copia: ", copia.datos.puntos); // [10, 20, 30]
Copia profunda con structuredClone
const original = { nivel: 1, datos: { puntos: [10, 20] } };
const copia = structuredClone(original);
copia.datos.puntos.push(30);
console.log("original:", original.datos.puntos); // [10, 20] intacto
console.log("copia: ", copia.datos.puntos); // [10, 20, 30]
Object.freeze impide mutar
const ajustes = Object.freeze({ volumen: 5 });
ajustes.volumen = 11; // ignorado silenciosamente
console.log(ajustes.volumen); // 5