Objetos en JavaScript: el error no está en la sintaxis, sino en confundir clave, referencia y copia

Un objeto de JavaScript puede parecer sencillo hasta que una modificación aparece en otro lugar, una clave llega desde un formulario o una copia cambia datos que nadie esperaba. El problema no suele ser olvidar la sintaxis de las llaves. Es confundir cuatro preguntas distintas: qué propiedad quieres leer, de dónde sale su nombre, quién es dueño de esa propiedad y si el valor que copias sigue compartiendo referencias con el original.

Esta diferencia importa más cuando un programa deja de ser un ejercicio aislado. Un usuario puede tener un perfil, una cesta puede contener productos y una configuración puede combinar valores por defecto con preferencias recibidas de una API. En los tres casos trabajarás con objetos, pero las decisiones no son iguales. La guía de objetos de MDN los presenta como entidades con propiedades y tipo; aquí daremos un paso adicional: aprender a detectar qué relación estás creando cada vez que accedes, recorres o copias uno.

El primer diagnóstico: ¿la clave está en el código o en los datos?

Imagina una función que recibe un usuario y debe mostrar su nombre:

const usuario = { nombre: "Ana", rol: "editora" };

console.log(usuario.nombre);

La notación de punto es clara porque nombre forma parte del programa. Si en cambio una tabla permite elegir el campo que se quiere mostrar, el nombre de la propiedad ya no está escrito como una decisión fija:

function leerCampo(registro, nombreDelCampo) {
  return registro[nombreDelCampo];
}

leerCampo(usuario, "rol");

Los dos accesos devuelven una propiedad, pero comunican intenciones diferentes. La referencia de property accessors de MDN distingue precisamente la notación de punto y la de corchetes. Usa punto cuando la clave es conocida y forma parte de la estructura que estás diseñando. Usa corchetes cuando la clave llega como dato, se calcula o contiene caracteres que no encajan en un identificador.

Un error típico es escribir registro.nombreDelCampo esperando que JavaScript sustituya la variable por su contenido. Esa expresión busca literalmente una propiedad llamada nombreDelCampo. Los corchetes no son una versión más avanzada de la notación de punto: son la señal de que el programa está separando la decisión del nombre y el valor concreto que se quiere consultar.

Una propiedad ausente no es igual que una propiedad vacía

Al leer una clave que no existe, JavaScript devuelve undefined. Eso es útil para explorar datos opcionales, pero también puede ocultar errores si la aplicación necesita distinguir entre ausencia y valor definido:

const preferencias = { tema: "oscuro" };

preferencias.idioma; // undefined
preferencias.idioma = ""; // existe, pero está vacío

Antes de poner una condición, decide qué significa cada estado en tu dominio. if (!preferencias.idioma) agrupa undefined, una cadena vacía, null y otros valores falsy. Si solo te interesa saber si hay una propiedad propia, puedes usar Object.hasOwn(preferencias, "idioma") en lugar de inferirlo a partir del valor. La diferencia evita que una preferencia válida pero vacía se trate como si nunca hubiera sido recibida.

También conviene no confiar ciegamente en nombres recibidos desde el exterior. Un objeto puede heredar propiedades de su prototipo, y un recorrido con for...in puede incluirlas. Para revisar los datos que el propio registro posee, Object.keys(), Object.values() y Object.entries() suelen expresar mejor la intención:

const campos = Object.entries(usuario);

for (const [clave, valor] of campos) {
  console.log(`${clave}: ${valor}`);
}

Recorrer no es modificar: elige la operación que pueda auditarse

Recorrer las propiedades puede servir para crear una vista, comprobar reglas o transformar una estructura. No significa que debas modificar el objeto durante el mismo recorrido. Separar lectura y escritura hace que el comportamiento sea más fácil de probar:

const etiquetas = Object.entries(usuario)
  .filter(([clave]) => clave !== "rol")
  .map(([clave, valor]) => `${clave}=${valor}`);

Este ejemplo crea una nueva lista de textos. No convierte el objeto en otra cosa ni altera usuario. Si necesitas actualizar un campo concreto, haz visible esa decisión:

const usuarioActualizado = {
  ...usuario,
  rol: "autora"
};

La propiedad rol aparece después del spread y su valor reemplaza el anterior. El orden es importante cuando varias fuentes tienen la misma clave. Es una forma cómoda de construir una nueva configuración, pero no es una solución universal para cualquier copia.

Spread crea una frontera nueva, no un mundo independiente

La documentación de spread syntax en MDN explica que el operador enumera propiedades y crea un objeto nuevo; también deja claro que la copia es superficial. Esa palabra es la que suele desaparecer de los tutoriales:

const perfil = {
  nombre: "Ana",
  preferencias: { tema: "oscuro" }
};

const copia = { ...perfil };
copia.preferencias.tema = "claro";

console.log(perfil.preferencias.tema); // claro

El objeto exterior es diferente, pero perfil.preferencias y copia.preferencias apuntan al mismo objeto interior. Si el dato es plano, spread puede ser suficiente. Si hay estructuras anidadas, decide si quieres compartir referencias, clonar ciertos niveles o usar una estrategia específica como structuredClone() cuando los tipos de datos lo permitan. No prometas una copia profunda solo porque la comparación entre objetos exteriores devuelve valores distintos.

La misma cautela vale para objetos que combinan datos de una API con valores predeterminados. Una composición como { ...predeterminados, ...recibidos } es legible, pero solo resuelve conflictos de primer nivel. Una clave anidada no se fusiona automáticamente:

const base = { editor: { tema: "claro", tamaño: 14 } };
const remoto = { editor: { tema: "oscuro" } };
const final = { ...base, ...remoto };

// final.editor.tamaño es undefined

En ese caso, la aplicación ha reemplazado el objeto editor entero. Si querías conservar tamaño, tendrías que expresar una composición por nivel. El fallo no es del operador: es una expectativa de fusión profunda que nunca se escribió en el código.

Una tabla de decisión para elegir la forma de trabajar

NecesidadHerramienta inicialPregunta de control
Leer una clave fijaNotación de punto¿El nombre pertenece al contrato del objeto?
Leer una clave recibidaNotación de corchetes¿El valor puede cambiar sin editar la función?
Listar datos propiosObject.keys() o Object.entries()¿Debo excluir propiedades heredadas?
Crear una varianteSpread¿Los valores anidados pueden compartir referencias?
Comprobar una propiedadObject.hasOwn()¿Ausente y vacío significan lo mismo?

La tabla no pretende reemplazar el razonamiento. Su valor está en obligarte a nombrar la relación que estás creando. Cuando una función falla, vuelve a la pregunta anterior: ¿el dato era una clave, una propiedad propia, una referencia compartida o una estructura que exigía otra política de copia?

Un ejercicio de depuración: sigue la referencia, no solo el valor

Para practicar, toma un objeto con una lista anidada y crea tres operaciones: una lectura, una actualización inmutable del primer nivel y una actualización deliberada de la lista interior. Después imprime el objeto original y la variante. Si ambos cambian, no asumas automáticamente que el lenguaje está copiando mal; comprueba qué nivel compartiste.

const pedido = {
  cliente: "Ana",
  items: [{ producto: "libro", cantidad: 1 }]
};

const revisado = {
  ...pedido,
  estado: "pendiente"
};

revisado.items[0].cantidad = 2;

console.log(pedido.estado); // undefined
console.log(pedido.items[0].cantidad); // 2

El resultado muestra dos fronteras distintas. Añadir estado no modifica el objeto exterior original; modificar un elemento dentro de items sí llega al original porque la lista y su elemento siguen compartidos. Si el pedido debe ser inmutable para una interfaz o un historial, actualiza también los niveles que cambian:

const seguro = {
  ...pedido,
  items: pedido.items.map((item, indice) =>
    indice === 0 ? { ...item, cantidad: 2 } : item
  )
};

Ahora la decisión es más larga, pero también más honesta: se creó una lista nueva y un objeto nuevo para el elemento modificado. El código permite revisar qué parte de la estructura cambió y cuál se conserva.

Cuando la clave viene de fuera, el contrato importa

Una clave dinámica puede llegar de una tabla, una URL o una respuesta de usuario. En ese punto, no basta con que los corchetes hagan funcionar la lectura. Decide qué nombres están permitidos y qué debe ocurrir cuando llega uno desconocido:

const camposVisibles = new Set(["nombre", "rol"]);

function mostrarCampo(usuario, clave) {
  if (!camposVisibles.has(clave)) {
    return undefined;
  }
  return Object.hasOwn(usuario, clave)
    ? usuario[clave]
    : undefined;
}

La lista permitida funciona como un contrato pequeño. Evita que una interfaz convierta cualquier texto recibido en una instrucción sobre qué parte del objeto debe exponer. También hace que la decisión sea revisable: cuando se agrega un campo, se modifica la lista y se puede probar el nuevo caso.

La misma idea aparece al combinar configuraciones. Si una fuente externa puede contener una clave que coincide con un valor predeterminado, el orden de los spreads decide quién gana. No dejes esa precedencia implícita. Escribe una prueba que cubra una clave desconocida, una clave propia vacía y una clave que intenta reemplazar una opción que el sistema debería controlar.

Los objetos son flexibles precisamente porque pueden representar estructuras cambiantes. Esa flexibilidad se vuelve una deuda cuando nadie define qué claves tienen significado, qué valores son aceptables y qué nivel de la estructura puede modificarse. El buen uso de objetos no elimina la variación: la coloca detrás de un límite que otra persona puede entender.

Conecta los objetos con el resto del lenguaje

Los objetos aparecen en formularios, respuestas de red, componentes y configuraciones. Nuestra guía sobre conceptos básicos de JavaScript ayuda a repasar valores, funciones y control de flujo antes de depurar estructuras más grandes. Si el objeto representa datos que terminarán en la interfaz, la guía sobre manipulación del DOM puede servir para separar la transformación de datos de la actualización visual.

También conviene recordar la relación con las colecciones. Cuando varios registros forman una lista, los métodos de arrays responden a otra clase de pregunta: transformar elementos, filtrar candidatos o encontrar una coincidencia. Consulta nuestra explicación sobre métodos de arrays en JavaScript para no usar un objeto como sustituto de una colección ordenada ni un array como diccionario improvisado.

Preguntas para comprobar tu modelo mental

¿La notación de punto y la de corchetes hacen siempre lo mismo?

Acceden a propiedades, pero no expresan la misma intención. El punto usa un nombre escrito en el código; los corchetes permiten que el nombre provenga de una variable o una expresión.

¿Spread clona un objeto completo?

No. Crea una copia superficial de las propiedades enumerables. Si hay objetos o arrays anidados, las referencias interiores pueden seguir compartidas.

¿Por qué for...in puede ser una mala elección?

Puede recorrer propiedades heredadas además de las propias. Si necesitas inspeccionar solo los datos del registro, considera Object.keys(), Object.entries() o una comprobación explícita de propiedad propia.

La próxima vez que escribas una llave, no preguntes solo qué sintaxis recuerdas. Pregunta qué contrato estás creando: una clave fija, un dato dinámico, una colección propia o una variante que todavía comparte referencias. Esa pregunta evita más errores que memorizar otra lista de métodos.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio