Hay un momento concreto en el aprendizaje de JavaScript donde todo se complica. Llevas un rato con variables, arrays, funciones... y de repente ves código así:
const usuario = {
nombre: 'Ana',
edad: 28,
saludar: function() {
return 'Hola, soy ' + this.nombre;
}
};
Y piensas: ¿qué es eso de this? ¿Por qué hay una función dentro de una variable? ¿Qué son esos dos puntos en vez de =?
Los objetos son el concepto que más cuesta pillar al principio y el que más se usa en la práctica. Todo en JavaScript es o usa objetos: los arrays, el DOM, las respuestas de una API, los componentes de React. Si no entiendes objetos, el resto se convierte en copiar código sin saber qué hace.
Antes de leer esto necesitas saber:
Por qué existen los objetos
Imagina que estás guardando información de un producto de una tienda online. Sin objetos lo harías así:
const productoNombre = 'Teclado mecánico';
const productoPrecio = 89.99;
const productoStock = 14;
const productoCategoria = 'Periféricos';
Cuatro variables para un producto. Si tienes 50 productos, son 200 variables. Y cuando quieras pasar ese producto a una función, tienes que pasar los cuatro parámetros por separado, en el orden correcto, sin equivocarte.
Un objeto agrupa todo eso en una sola cosa:
const producto = {
nombre: 'Teclado mecánico',
precio: 89.99,
stock: 14,
categoria: 'Periféricos'
};
Ahora producto es una sola variable que contiene todo lo relacionado con ese producto. Puedes pasarla a funciones, guardarla en un array junto a otros productos, devolverla desde una API. Todo junto, todo relacionado.
Cada par nombre: valor se llama propiedad. El nombre de la propiedad se llama clave. El conjunto de llaves {} es el objeto.
Crear un objeto
La forma más habitual, y la que vas a ver en el 95% del código, es la notación literal con llaves:
const pelicula = {
titulo: 'El señor de los anillos',
anio: 2001,
duracion: 178,
disponible: true,
generos: ['Fantasía', 'Aventura']
};
Las propiedades pueden ser de cualquier tipo: string, número, booleano, array, incluso otro objeto. Y un objeto vacío también es válido: const vacio = {};
Leer y modificar propiedades
Hay dos formas de acceder a una propiedad. La primera es con punto:
console.log(pelicula.titulo); // 'El señor de los anillos'
console.log(pelicula.duracion); // 178
console.log(pelicula.generos[0]); // 'Fantasía' (el primer elemento del array)
La segunda es con corchetes. Aquí es donde la mayoría no ve para qué sirve, porque parece lo mismo. La diferencia es que los corchetes aceptan variables:
const campo = 'titulo';
console.log(pelicula[campo]); // 'El señor de los anillos'
// Útil cuando el nombre de la propiedad viene de fuera
function leerPropiedad(objeto, nombrePropiedad) {
return objeto[nombrePropiedad];
}
leerPropiedad(pelicula, 'anio'); // 2001
leerPropiedad(pelicula, 'titulo'); // 'El señor de los anillos'
También sirven cuando la clave tiene caracteres que no puede tener un identificador normal, como guiones: objeto['mi-propiedad']. Con punto no podrías porque JavaScript lo interpretaría como una resta.
Para modificar una propiedad es igual que asignar una variable:
pelicula.disponible = false;
pelicula.duracion = 228; // versión extendida
Y para añadir una propiedad que no existía antes, lo mismo — simplemente la asignas:
pelicula.director = 'Peter Jackson';
console.log(pelicula.director); // 'Peter Jackson'
Para eliminar una propiedad existe delete:
delete pelicula.disponible;
console.log(pelicula.disponible); // undefined
⚠️ El error más común al empezar:
Usar const y creer que el objeto no se puede modificar. const impide que reasignes la variable (pelicula = otraCosa), pero las propiedades del objeto sí se pueden cambiar. Para un objeto completamente inmutable necesitas Object.freeze(), que es otro tema.
Comprobar si una propiedad existe
Cuando lees una propiedad que no existe, JavaScript no lanza un error — devuelve undefined. Esto confunde porque parece que todo va bien:
console.log(pelicula.presupuesto); // undefined — no existe, pero no hay error
Si luego intentas operar con ese undefined, ahí sí explota. Para comprobar si una propiedad existe antes de usarla tienes dos opciones:
// Opción 1: comparar con undefined
if (pelicula.presupuesto !== undefined) {
console.log(pelicula.presupuesto);
}
// Opción 2: operador in (más explícito)
if ('presupuesto' in pelicula) {
console.log(pelicula.presupuesto);
}
// Opción 3: encadenamiento opcional — si no existe, devuelve undefined sin error
console.log(pelicula?.presupuesto); // undefined
El encadenamiento opcional ?. es especialmente útil con objetos anidados, que vemos más adelante.
Métodos: funciones dentro de un objeto
Un objeto puede tener funciones como propiedades. A esas funciones se les llama métodos. Ya los usas sin saberlo: console.log() es el método log del objeto console. array.push() es el método push de ese array.
Para añadir un método a tu propio objeto:
const producto = {
nombre: 'Auriculares',
precio: 49.50,
precioConIVA: function() {
return producto.precio * 1.21;
}
};
console.log(producto.precioConIVA()); // 59.895
Funciona, pero tiene un problema: estás referenciando el objeto por su nombre (producto.precio) dentro del propio método. Si en algún momento renombras la variable o copias el objeto con otro nombre, el método se rompe. Para eso existe this.
this: el objeto que llama al método
Dentro de un método, this es una referencia al objeto que contiene ese método. En vez de escribir el nombre del objeto, escribes this y JavaScript lo resuelve en tiempo de ejecución:
const producto = {
nombre: 'Auriculares',
precio: 49.50,
precioConIVA: function() {
return this.precio * 1.21; // this = producto
},
descripcion: function() {
return this.nombre + ' — ' + this.precio + '€ (sin IVA)';
}
};
console.log(producto.precioConIVA()); // 59.895
console.log(producto.descripcion()); // 'Auriculares — 49.5€ (sin IVA)'
⚠️ this no funciona igual en arrow functions:
Si escribes el método como arrow function, this no apunta al objeto — apunta al contexto exterior (normalmente window o undefined en modo estricto). Para métodos de objetos, usa siempre function normal, no arrow function.
const mal = {
valor: 10,
// ❌ this no apunta al objeto aquí
doble: () => this.valor * 2
};
const bien = {
valor: 10,
// ✅ this sí apunta al objeto
doble: function() { return this.valor * 2; }
};
Objetos dentro de objetos
Las propiedades de un objeto pueden ser otros objetos. Esto es lo que en la documentación llaman objetos anidados y es lo que ves constantemente en las respuestas de las APIs:
const pedido = {
id: 'PED-2847',
cliente: {
nombre: 'Carlos Ruiz',
email: 'carlos@email.com',
direccion: {
calle: 'Calle Mayor 14',
ciudad: 'Madrid',
codigoPostal: '28001'
}
},
total: 127.45
};
// Para llegar al código postal, encadenas puntos
console.log(pedido.cliente.nombre); // 'Carlos Ruiz'
console.log(pedido.cliente.direccion.ciudad); // 'Madrid'
console.log(pedido.cliente.direccion.codigoPostal); // '28001'
Aquí es donde el encadenamiento opcional ?. se vuelve importante. Si en algún punto de la cadena hay un null o undefined, sin ?. obtienes un error que rompe la ejecución:
// Si pedido.cliente fuera null, esto lanza TypeError
console.log(pedido.cliente.direccion.ciudad); // ❌ TypeError
// Con ?. devuelve undefined en vez de error
console.log(pedido?.cliente?.direccion?.ciudad); // ✅ undefined, sin error
Esto lo vas a usar mucho cuando trabajas con datos de APIs, donde no siempre sabes qué campos vienen rellenos y cuáles no. Si has trabajado con Fetch API, habrás visto que la respuesta JSON suele ser un objeto con propiedades anidadas exactamente así.
Arrays de objetos
En la práctica, casi nunca tienes un solo objeto. Tienes un array de objetos: lista de productos, lista de usuarios, lista de pedidos. Es la estructura de datos más habitual en cualquier aplicación:
const carrito = [
{ nombre: 'Teclado', precio: 89.99, cantidad: 1 },
{ nombre: 'Ratón', precio: 34.50, cantidad: 2 },
{ nombre: 'Alfombrilla', precio: 9.99, cantidad: 1 }
];
// Acceder a un elemento concreto
console.log(carrito[0].nombre); // 'Teclado'
console.log(carrito[1].precio); // 34.50
// Calcular el total del carrito con reduce
const total = carrito.reduce((suma, item) => suma + item.precio * item.cantidad, 0);
console.log(total); // 169.98
// Filtrar solo los que cuestan más de 20€
const caros = carrito.filter(item => item.precio > 20);
// [{ nombre: 'Teclado', ... }, { nombre: 'Ratón', ... }]
// Obtener solo los nombres con map
const nombres = carrito.map(item => item.nombre);
// ['Teclado', 'Ratón', 'Alfombrilla']
Fíjate cómo filter y map encajan perfectamente con arrays de objetos. Lo que en el post de métodos de array parecía abstracto, aquí tiene sentido inmediato: iteras sobre objetos y accedes a sus propiedades dentro del callback.
Recorrer las propiedades de un objeto
A veces no sabes de antemano cuántas propiedades tiene un objeto, o quieres procesarlas todas sin repetirte. Para eso existen tres métodos estáticos de Object:
const precios = {
basico: 0,
estandar: 9.99,
profesional: 19.99,
empresa: 49.99
};
// Object.keys() — array con los nombres de las propiedades
console.log(Object.keys(precios));
// ['basico', 'estandar', 'profesional', 'empresa']
// Object.values() — array con los valores
console.log(Object.values(precios));
// [0, 9.99, 19.99, 49.99]
// Object.entries() — array de pares [clave, valor]
console.log(Object.entries(precios));
// [['basico', 0], ['estandar', 9.99], ['profesional', 19.99], ['empresa', 49.99]]
Como devuelven arrays, puedes encadenarles directamente forEach, map o filter:
// Mostrar los planes que cuestan más de 0€
Object.entries(precios)
.filter(([plan, precio]) => precio > 0)
.forEach(([plan, precio]) => {
console.log(plan + ': ' + precio + '€/mes');
});
// estandar: 9.99€/mes
// profesional: 19.99€/mes
// empresa: 49.99€/mes
Esa sintaxis ([plan, precio]) en el parámetro es desestructuración de array — la vemos en detalle en el siguiente post de esta serie.
Copiar objetos: el error que comete todo el mundo
Este es el sitio donde más gente se quema. Cuando copias un objeto con =, no estás copiando el objeto — estás copiando la referencia. Ambas variables apuntan al mismo objeto en memoria:
const original = { nombre: 'Ana', edad: 28 };
const copia = original; // ❌ NO es una copia, es el mismo objeto
copia.nombre = 'Carlos';
console.log(original.nombre); // 'Carlos' — también cambió el original
Para hacer una copia real de un objeto plano (sin objetos anidados), usa Object.assign() o spread. El spread lo vemos en el siguiente post, pero adelanto la forma más habitual:
// ✅ Copia superficial con Object.assign
const copia2 = Object.assign({}, original);
copia2.nombre = 'Carlos';
console.log(original.nombre); // 'Ana' — el original no cambia
// ✅ Copia superficial con spread (la forma moderna)
const copia3 = { ...original };
"Superficial" significa que funciona bien con objetos de un solo nivel. Si el objeto tiene propiedades que son otros objetos, esas propiedades internas siguen siendo referencias compartidas. Para una copia profunda necesitas structuredClone(), disponible desde 2022 en todos los navegadores modernos:
// ✅ Copia profunda — funciona con objetos anidados
const copiaCompleta = structuredClone(pedido);
Proyecto: gestor de contactos
Nada como construir algo concreto. Un gestor de contactos por consola que usa todo lo que hemos visto: crear objetos, leerlos, modificarlos, buscarlos en un array y recorrer sus propiedades.
Copia esto en un archivo contactos.js y ejecútalo con node contactos.js — o pégalo en la consola del navegador:
// Base de datos en memoria — array de objetos
const agenda = [
{
id: 1,
nombre: 'Ana García',
telefono: '612 345 678',
email: 'ana@email.com',
tags: ['trabajo', 'diseño']
},
{
id: 2,
nombre: 'Marcos López',
telefono: '634 987 654',
email: 'marcos@email.com',
tags: ['amigo']
}
];
// Añadir un contacto nuevo
function añadirContacto(nombre, telefono, email, tags = []) {
const nuevoContacto = {
id: agenda.length + 1,
nombre: nombre,
telefono: telefono,
email: email,
tags: tags
};
agenda.push(nuevoContacto);
return nuevoContacto;
}
// Buscar por nombre (parcial, sin distinguir mayúsculas)
function buscar(texto) {
return agenda.filter(c =>
c.nombre.toLowerCase().includes(texto.toLowerCase())
);
}
// Actualizar cualquier campo de un contacto por id
function actualizar(id, cambios) {
const contacto = agenda.find(c => c.id === id);
if (!contacto) return null;
Object.assign(contacto, cambios);
return contacto;
}
// Listar todos mostrando sus propiedades
function listar() {
agenda.forEach(contacto => {
console.log('---');
Object.entries(contacto).forEach(([clave, valor]) => {
console.log(clave + ': ' + valor);
});
});
}
// Pruébalo:
añadirContacto('Laura Sanz', '699 111 222', 'laura@email.com', ['trabajo']);
actualizar(1, { telefono: '612 000 001' });
console.log(buscar('ana'));
listar();
La función actualizar usa Object.assign(contacto, cambios): copia las propiedades de cambios sobre el objeto contacto, sobrescribiendo solo las que existen en cambios. El resto de propiedades del contacto se quedan intactas.
Lo que acabas de usar sin darte cuenta:
- Notación literal para crear objetos
- Acceso con punto desde funciones externas
Object.assignpara actualizar propiedades selectivamenteObject.entries+forEachpara recorrer propiedades dinámicamentefindyfiltersobre un array de objetos
El siguiente paso
Con objetos claros, el siguiente tema que desbloquea un montón de cosas es la desestructuración y el operador spread. Esa sintaxis const { nombre } = persona que ves en todos los tutoriales de React, o el ...objeto que aparece cuando copias o fusionas datos — eso ya lo puedes aprender sin que parezca magia.