Módulo 07: Objetos
Objetivos del módulo
- Crear objetos literales y acceder a sus propiedades con punto y con corchetes
- Añadir, modificar, borrar y comprobar propiedades de forma segura
- Definir métodos y entender cómo funciona
this(y cómo no perderlo) - Recorrer objetos con
for...in,Object.keys,Object.valuesyObject.entries - Dominar la desestructuración y el spread de objetos
- Comprender las referencias y la diferencia entre copia superficial y profunda (
structuredClone) - Proteger objetos con
Object.freeze,Object.sealyObject.preventExtensions - Usar getters, setters y descriptores de propiedad
- Convertir objetos a JSON y de vuelta, incluidas las novedades de ES2026
- Manejar datos anidados de una API con
?.y??
1. ¿Qué es un objeto?
📘 Concepto: Un objeto es una colección de pares clave → valor. Cada par se llama propiedad. Si el valor es una función, la propiedad se llama método.
Si un array es una estantería numerada, un objeto es una ficha con apartados con nombre: la ficha de un alumno tiene “nombre”, “edad” y “curso”. No te importa en qué orden están: buscas cada dato por su nombre.
const alumno = {
nombre: 'Lucía',
edad: 21,
curso: 'DAW',
matriculada: true,
asignaturas: ['Programación', 'Bases de datos'],
direccion: { ciudad: 'Valencia', cp: '46001' },
};
console.log(alumno.nombre); // Lucía
console.log(alumno.asignaturas[0]); // Programación
console.log(alumno.direccion.ciudad); // Valencia
| Uso | Array | Objeto |
|---|---|---|
| Lista de elementos del mismo tipo | ✅ ['Ana', 'Luis'] | ❌ |
| Datos de una entidad con campos distintos | ❌ | ✅ { nombre: 'Ana', edad: 30 } |
| Acceso por | Posición ([0]) | Nombre (.nombre) |
| Orden | Importante | Normalmente irrelevante |
💡 Consejo: Lo más habitual en aplicaciones reales es combinar ambos: arrays de objetos (una lista de productos, de usuarios, de pedidos…).
2. Propiedades: crear y acceder
2.1. Punto frente a corchetes
const coche = { marca: 'Seat', modelo: 'León', 'año-fabricación': 2024 };
// Notación de punto: la más legible
console.log(coche.marca); // Seat
// Notación de corchetes: la clave es una expresión (normalmente un texto)
console.log(coche['modelo']); // León
console.log(coche['año-fabricación']); // 2024 (con punto sería un error de sintaxis)
// Corchetes con una variable: acceso dinámico
const campo = 'marca';
console.log(coche[campo]); // Seat
console.log(coche.campo); // undefined (busca una propiedad llamada "campo")
| Usa punto cuando… | Usa corchetes cuando… |
|---|---|
| Conoces el nombre al escribir el código | El nombre está en una variable o se calcula |
| El nombre es un identificador válido | El nombre tiene espacios, guiones o empieza por número |
📘 Concepto: Las claves de un objeto son siempre cadenas (o símbolos, módulo 11). Si usas un número como clave, se convierte a texto:
obj[1]yobj['1']son la misma propiedad.
2.2. Shorthand de propiedades
Cuando la clave y la variable se llaman igual, puedes escribirla una sola vez:
const nombre = 'Ana';
const edad = 30;
const persona1 = { nombre: nombre, edad: edad }; // forma larga
const persona2 = { nombre, edad }; // shorthand (ES2015)
console.log(persona2); // { nombre: 'Ana', edad: 30 }
2.3. Propiedades computadas
Entre corchetes, dentro del literal, puedes calcular el nombre de la clave:
const idioma = 'es';
const traducciones = {
[`saludo_${idioma}`]: 'Hola',
[`despedida_${idioma}`]: 'Adiós',
};
console.log(traducciones); // { saludo_es: 'Hola', despedida_es: 'Adiós' }
// Muy útil para funciones genéricas
function crearFiltro(campo, valor) {
return { [campo]: valor };
}
console.log(crearFiltro('ciudad', 'Bilbao')); // { ciudad: 'Bilbao' }
3. Añadir, modificar, borrar y comprobar
const producto = { nombre: 'Taza', precio: 6 };
producto.precio = 7.5; // modificar
producto.stock = 120; // añadir (si no existe, se crea)
producto['color'] = 'blanco'; // añadir con corchetes
delete producto.color; // borrar
console.log(producto); // { nombre: 'Taza', precio: 7.5, stock: 120 }
console.log(producto.descuento); // undefined (no existe: no da error)
⚠️ Importante: Igual que con los arrays,
constimpide reasignar la variable (producto = {}), pero no impide cambiar sus propiedades. Para eso existeObject.freeze(sección 10).
3.1. ¿Existe esta propiedad?
const config = { tema: 'oscuro', volumen: 0, avatar: undefined };
// 1) Comparar con undefined: falla si la propiedad existe con valor undefined
console.log(config.avatar !== undefined); // false (¡pero la propiedad existe!)
// 2) Operador in: incluye propiedades heredadas del prototipo
console.log('volumen' in config); // true
console.log('toString' in config); // true (heredada de Object.prototype)
// 3) Object.hasOwn (ES2022): solo propiedades propias. La opción recomendada
console.log(Object.hasOwn(config, 'volumen')); // true
console.log(Object.hasOwn(config, 'toString')); // false
console.log(Object.hasOwn(config, 'avatar')); // true
| Forma | Propias | Heredadas | Detecta valor undefined |
|---|---|---|---|
obj.prop !== undefined | Sí | Sí | No |
'prop' in obj | Sí | Sí | Sí |
Object.hasOwn(obj, 'prop') | Sí | No | Sí |
obj.hasOwnProperty('prop') | Sí | No | Sí (forma antigua; falla en objetos sin prototipo) |
⚠️ Importante: No compruebes con
if (config.volumen): el valor0es falsy y la condición fallaría aunque la propiedad exista. Lo mismo ocurre con''yfalse.
3.2. Encadenamiento opcional ?.
Si accedes a una propiedad de algo que es undefined o null, obtienes un TypeError. El operador ?. (módulo 03) corta la cadena y devuelve undefined:
const usuario = { nombre: 'Luis' };
// console.log(usuario.direccion.ciudad); // TypeError: Cannot read properties of undefined
console.log(usuario.direccion?.ciudad); // undefined
console.log(usuario.direccion?.ciudad ?? '—'); // —
4. Métodos
const calculadora = {
marca: 'Casio',
// Forma clásica
sumar: function (a, b) {
return a + b;
},
// Shorthand de métodos (ES2015): la forma recomendada
restar(a, b) {
return a - b;
},
};
console.log(calculadora.sumar(2, 3)); // 5
console.log(calculadora.restar(9, 4)); // 5
4.1. this en los métodos
📘 Concepto: Dentro de un método,
thises el objeto a través del cual se ha llamado al método: lo que hay a la izquierda del punto en el momento de la llamada.
const cuenta = {
titular: 'Marta',
saldo: 100,
ingresar(cantidad) {
this.saldo += cantidad;
return this; // devolver this permite encadenar llamadas
},
resumen() {
return `${this.titular} tiene ${this.saldo} €`;
},
};
cuenta.ingresar(50).ingresar(25);
console.log(cuenta.resumen()); // Marta tiene 175 €
4.2. Trampa 1: perder this al separar el método
this no se fija al escribir el método, sino al llamarlo. Si guardas el método en una variable o lo pasas como callback, pierde su objeto:
const perro = {
nombre: 'Toby',
ladrar() {
return `${this.nombre} dice ¡guau!`;
},
};
console.log(perro.ladrar()); // Toby dice ¡guau!
const suelto = perro.ladrar; // copiamos la función, no el objeto
try {
suelto(); // llamada sin objeto delante: this es undefined
} catch (error) {
console.log(error.message); // Cannot read properties of undefined (reading 'nombre')
}
⚠️ Importante: En los módulos ES y en modo estricto, una función llamada “suelta” recibe
this === undefined, y el error salta enseguida. En scripts antiguos sin modo estricto,thissería el objeto global (window) y el fallo pasaría desapercibido. Esto pasa también al hacersetTimeout(perro.ladrar, 1000)oboton.addEventListener('click', perro.ladrar).
4.3. Trampa 2: arrow functions como métodos
Las arrow functions no tienen this propio: usan el this del lugar donde se escribieron. Por eso no sirven como métodos, pero son perfectas dentro de un método:
const temporizador = {
segundos: 0,
// ❌ Como método: this NO es temporizador
mal: () => typeof this,
// ✅ Método normal con arrow dentro: la arrow hereda el this del método
iniciar() {
const intervalo = setInterval(() => {
this.segundos++;
console.log(`Segundos: ${this.segundos}`);
if (this.segundos === 3) clearInterval(intervalo);
}, 100);
},
};
console.log(temporizador.mal()); // undefined (el this del módulo, no el objeto)
temporizador.iniciar();
// Segundos: 1
// Segundos: 2
// Segundos: 3
4.4. Fijar this a mano: call, apply y bind
function presentar(saludo, despedida) {
return `${saludo}, soy ${this.nombre}. ${despedida}`;
}
const ana = { nombre: 'Ana' };
// call: llama YA con un this concreto; argumentos separados por comas
console.log(presentar.call(ana, 'Hola', 'Adiós')); // Hola, soy Ana. Adiós
// apply: igual, pero los argumentos van en un array
console.log(presentar.apply(ana, ['Buenas', 'Chao'])); // Buenas, soy Ana. Chao
// bind: NO llama; devuelve una función nueva con this fijado para siempre
const presentarAna = presentar.bind(ana, 'Hey');
console.log(presentarAna('Nos vemos')); // Hey, soy Ana. Nos vemos
// Solución a la trampa 1
const perro = { nombre: 'Toby', ladrar() { return `${this.nombre}: ¡guau!`; } };
const ladrarToby = perro.ladrar.bind(perro);
console.log(ladrarToby()); // Toby: ¡guau!
| Método | ¿Ejecuta la función? | Argumentos | Uso típico |
|---|---|---|---|
fn.call(obj, a, b) | Sí, inmediatamente | Separados | Reutilizar un método con otro objeto |
fn.apply(obj, [a, b]) | Sí, inmediatamente | En un array | Igual, cuando ya tienes los argumentos en un array |
fn.bind(obj, a) | No, devuelve una función | Fija los primeros (opcional) | Callbacks y eventos |
💡 Consejo: Regla práctica: usa métodos shorthand para definir métodos, arrow functions para callbacks dentro de ellos, y
bind(o una arrow que envuelva la llamada,() => perro.ladrar()) cuando pases un método como callback. Volveremos athiscon las clases en el módulo 09.
5. Recorrer objetos
const precios = { pan: 1.2, leche: 0.95, huevos: 2.4 };
console.log(Object.keys(precios)); // ['pan', 'leche', 'huevos']
console.log(Object.values(precios)); // [1.2, 0.95, 2.4]
console.log(Object.entries(precios)); // [['pan', 1.2], ['leche', 0.95], ['huevos', 2.4]]
// La forma moderna de recorrer: for...of + entries + desestructuración
for (const [producto, precio] of Object.entries(precios)) {
console.log(`${producto}: ${precio.toFixed(2)} €`);
}
// pan: 1.20 €
// leche: 0.95 €
// huevos: 2.40 €
// Sumar todos los valores
const total = Object.values(precios).reduce((s, p) => s + p, 0);
console.log(total.toFixed(2)); // 4.55
5.1. for...in
for...in recorre las claves enumerables, incluidas las heredadas del prototipo. Por eso, si lo usas, suele acompañarse de Object.hasOwn:
for (const clave in precios) {
if (Object.hasOwn(precios, clave)) {
console.log(clave, precios[clave]);
}
}
💡 Consejo: En código moderno, prefiere
Object.keys/values/entriesconfor...ofo con métodos de array: solo devuelven propiedades propias y te dan acceso amap,filter, etc.
5.2. Object.fromEntries: de pares a objeto
Es la operación inversa de Object.entries. Juntas permiten “transformar objetos como si fueran arrays”:
const precios = { pan: 1.2, leche: 0.95, huevos: 2.4 };
// Subir todos los precios un 10 %
const subidos = Object.fromEntries(
Object.entries(precios).map(([clave, valor]) => [clave, +(valor * 1.1).toFixed(2)]),
);
console.log(subidos); // { pan: 1.32, leche: 1.04, huevos: 2.64 }
// (0.95 × 1.1 = 1.045, pero en binario se guarda como 1.04499... y toFixed redondea a 1.04)
// Quedarse solo con los productos de más de 1 €
const caros = Object.fromEntries(Object.entries(precios).filter(([, valor]) => valor > 1));
console.log(caros); // { pan: 1.2, huevos: 2.4 }
// También convierte un Map o los parámetros de una URL en objeto
const params = new URLSearchParams('pagina=2&orden=precio');
console.log(Object.fromEntries(params)); // { pagina: '2', orden: 'precio' }
📘 Concepto: ¿En qué orden salen las claves? Primero las claves que parecen números enteros, en orden ascendente; después el resto de cadenas, en el orden de inserción.
Object.keys({ b: 1, 2: 1, a: 1, 1: 1 })devuelve['1', '2', 'b', 'a'].
6. Desestructuración de objetos
📘 Concepto: Igual que con los arrays, pero extrayendo por nombre de propiedad en lugar de por posición.
const pelicula = {
titulo: 'El laberinto del fauno',
anio: 2006,
director: { nombre: 'Guillermo del Toro', pais: 'México' },
generos: ['fantasía', 'drama'],
};
// Básica: variables con el mismo nombre que las propiedades
const { titulo, anio } = pelicula;
console.log(titulo, anio); // El laberinto del fauno 2006
// Renombrar: propiedad: nuevoNombre
const { titulo: nombrePeli } = pelicula;
console.log(nombrePeli); // El laberinto del fauno
// Valores por defecto (si la propiedad es undefined)
const { duracion = 'desconocida', anio: estreno = 2000 } = pelicula;
console.log(duracion, estreno); // desconocida 2006
// Anidada
const { director: { nombre: nombreDirector, pais } } = pelicula;
console.log(nombreDirector, pais); // Guillermo del Toro México
// Mezclada con arrays
const { generos: [generoPrincipal] } = pelicula;
console.log(generoPrincipal); // fantasía
// Rest: el resto de propiedades en un objeto nuevo
const { director, generos, ...datosBasicos } = pelicula;
console.log(datosBasicos); // { titulo: 'El laberinto del fauno', anio: 2006 }
⚠️ Importante: En la desestructuración anidada,
directorno se crea como variable cuando escribes{ director: { nombre } }: solo indica el camino. Y sipelicula.directorfueraundefined, se lanzaría unTypeError. Protégete con un valor por defecto:{ director: { nombre } = {} }.
6.1. Desestructuración en parámetros: “parámetros con nombre”
Es uno de los patrones más usados en JavaScript profesional: en vez de recordar el orden de 5 argumentos, la función recibe un objeto de opciones.
function crearUsuario({ nombre, email, rol = 'lector', activo = true } = {}) {
return { nombre, email, rol, activo };
}
console.log(crearUsuario({ nombre: 'Ana', email: 'ana@correo.es' }));
// { nombre: 'Ana', email: 'ana@correo.es', rol: 'lector', activo: true }
console.log(crearUsuario({ email: 'luis@correo.es', rol: 'admin', nombre: 'Luis' }));
// { nombre: 'Luis', email: 'luis@correo.es', rol: 'admin', activo: true }
console.log(crearUsuario()); // { nombre: undefined, email: undefined, rol: 'lector', activo: true }
💡 Consejo: El
= {}final permite llamar a la función sin argumentos. Sin él,crearUsuario()lanzaríaTypeErroral intentar desestructurarundefined.
7. Spread de objetos, fusión y Object.assign
El operador ... también funciona con objetos: copia sus propiedades propias enumerables en un objeto nuevo.
const base = { color: 'azul', talla: 'M' };
const copia = { ...base };
const ampliado = { ...base, precio: 20 };
const modificado = { ...base, talla: 'L' }; // las propiedades posteriores ganan
console.log(ampliado); // { color: 'azul', talla: 'M', precio: 20 }
console.log(modificado); // { color: 'azul', talla: 'L' }
console.log(base); // { color: 'azul', talla: 'M' } (intacto)
// Fusionar varios objetos: si una clave se repite, gana el último
const a = { x: 1, y: 2 };
const b = { y: 20, z: 30 };
console.log({ ...a, ...b }); // { x: 1, y: 20, z: 30 }
console.log({ ...b, ...a }); // { y: 2, z: 30, x: 1 }
// Añadir una propiedad solo si se cumple una condición
const esAdmin = true;
const permisos = { leer: true, ...(esAdmin && { borrar: true }) };
console.log(permisos); // { leer: true, borrar: true }
Object.assign(destino, ...origenes) hace algo parecido, pero modifica el destino:
const destino = { a: 1 };
const resultado = Object.assign(destino, { b: 2 }, { c: 3 });
console.log(destino); // { a: 1, b: 2, c: 3 } ← modificado
console.log(resultado === destino); // true
// Para no modificar nada, pasa un objeto vacío como destino
const fusion = Object.assign({}, { a: 1 }, { b: 2 });
{ ...a, ...b } | Object.assign(a, b) | |
|---|---|---|
| Crea objeto nuevo | Sí | No (modifica a) |
| Ejecuta setters del destino | No (define propiedades) | Sí (asigna) |
| Legibilidad | Mayor | Menor |
| Uso recomendado | Por defecto | Modificar un objeto existente a propósito |
8. Referencias y comparación
📘 Concepto: Los primitivos (números, textos, booleanos…) se copian por valor. Los objetos (incluidos arrays y funciones) se manejan por referencia: la variable guarda la “dirección” del objeto, no el objeto en sí.
// Primitivos: copia independiente
let x = 10;
let y = x;
y = 20;
console.log(x); // 10
// Objetos: dos variables, un único objeto
const original = { puntos: 10 };
const alias = original;
alias.puntos = 99;
console.log(original.puntos); // 99
// Comparación: === compara referencias, no contenido
console.log({ a: 1 } === { a: 1 }); // false (dos objetos distintos)
console.log(original === alias); // true (el mismo objeto)
flowchart LR
V1["original"] --> O["{ puntos: 99 }"]
V2["alias"] --> O
V3["otro = { ...original }"] --> O2["{ puntos: 99 } (copia)"]
Lo mismo ocurre al pasar un objeto a una función: la función recibe la referencia y puede modificar el original.
function aplicarDescuento(producto) {
producto.precio *= 0.9; // ⚠️ modifica el objeto de quien llama
}
const camiseta = { precio: 20 };
aplicarDescuento(camiseta);
console.log(camiseta.precio); // 18
// Versión sin efectos secundarios: devolver un objeto nuevo
const conDescuento = (producto) => ({ ...producto, precio: producto.precio * 0.9 });
const pantalon = { precio: 40 };
console.log(conDescuento(pantalon), pantalon); // { precio: 36 } { precio: 40 }
💡 Consejo: Para comparar el contenido de dos objetos sencillos, puedes comparar sus propiedades una a una o, para datos simples sin funciones ni fechas,
JSON.stringify(a) === JSON.stringify(b)(ojo: depende del orden de las claves). En proyectos grandes se usa una función de igualdad profunda, comoisDeepStrictEqualdel módulonode:utilen Node.
9. Copia superficial frente a profunda: structuredClone
const pedido = {
id: 7,
cliente: { nombre: 'Eva' },
lineas: [{ producto: 'Libro', unidades: 1 }],
fecha: new Date('2026-10-01T10:00:00Z'),
};
// Copia superficial: el primer nivel es nuevo, lo anidado se comparte
const superficial = { ...pedido };
superficial.id = 8;
superficial.cliente.nombre = 'EVA';
console.log(pedido.id, pedido.cliente.nombre); // 7 EVA ← ¡el cliente es compartido!
// Copia profunda: todo es nuevo
const profunda = structuredClone(pedido);
profunda.lineas[0].unidades = 5;
console.log(pedido.lineas[0].unidades); // 1 (intacto)
console.log(profunda.fecha instanceof Date); // true (las fechas se conservan)
structuredClone (disponible en navegadores y en Node) usa el algoritmo de clonado estructurado: copia objetos, arrays, Date, Map, Set, RegExp, referencias circulares… pero no funciones ni elementos del DOM.
try {
structuredClone({ saludar() {} });
} catch (error) {
console.log(error.name); // DataCloneError
}
⚠️ Importante:
structuredClonetampoco conserva el prototipo de las instancias de clase: un objeto creado connew Producto()se clona como objeto plano. Lo verás en el módulo 09.
10. Inmutabilidad: freeze, seal y preventExtensions
const ajustes = Object.freeze({ idioma: 'es', tema: 'claro' });
try {
ajustes.tema = 'oscuro'; // en modo estricto (módulos) lanza TypeError
} catch (error) {
console.log(error.name); // TypeError
}
console.log(ajustes.tema); // claro
console.log(Object.isFrozen(ajustes)); // true
| Función | Añadir | Borrar | Modificar valores | Comprobar con |
|---|---|---|---|---|
Object.preventExtensions(obj) | ❌ | ✅ | ✅ | Object.isExtensible |
Object.seal(obj) | ❌ | ❌ | ✅ | Object.isSealed |
Object.freeze(obj) | ❌ | ❌ | ❌ | Object.isFrozen |
const sellado = Object.seal({ x: 1 });
sellado.x = 2; // permitido
console.log(sellado.x); // 2
try {
delete sellado.x; // no permitido
} catch (error) {
console.log('No se puede borrar en un objeto sellado');
}
⚠️ Importante:
Object.freezees superficial: congela el primer nivel, pero los objetos anidados siguen siendo modificables.
const congelado = Object.freeze({ datos: { contador: 0 } });
congelado.datos.contador++; // ¡funciona!
console.log(congelado.datos.contador); // 1
// Congelado profundo recursivo
function congelarProfundo(obj) {
for (const valor of Object.values(obj)) {
if (typeof valor === 'object' && valor !== null) congelarProfundo(valor);
}
return Object.freeze(obj);
}
const constantes = congelarProfundo({ iva: { general: 21, reducido: 10 } });
console.log(Object.isFrozen(constantes.iva)); // true
💡 Consejo:
Object.freezees ideal para constantes de configuración y para “enumeraciones”:const Estado = Object.freeze({ PENDIENTE: 'pendiente', ENVIADO: 'enviado' }).
11. Getters y setters
Un getter es una propiedad que se calcula al leerla; un setter, una que ejecuta código al asignarla. Desde fuera se usan como propiedades normales, sin paréntesis.
const empleado = {
nombre: 'Carmen',
apellidos: 'Ruiz Soto',
_salario: 2000, // el guion bajo indica "uso interno" (solo una convención)
get nombreCompleto() {
return `${this.nombre} ${this.apellidos}`;
},
set nombreCompleto(valor) {
[this.nombre, ...this.apellidos] = valor.split(' ');
this.apellidos = this.apellidos.join(' ');
},
get salario() {
return `${this._salario.toLocaleString('es-ES')} €`;
},
set salario(valor) {
if (typeof valor !== 'number' || valor < 0) {
throw new RangeError('El salario debe ser un número positivo');
}
this._salario = valor;
},
};
console.log(empleado.nombreCompleto); // Carmen Ruiz Soto
empleado.nombreCompleto = 'Pablo García Lima';
console.log(empleado.nombre, '|', empleado.apellidos); // Pablo | García Lima
empleado.salario = 2350;
console.log(empleado.salario); // 2350 €
try {
empleado.salario = -5;
} catch (error) {
console.log(error.message); // El salario debe ser un número positivo
}
📘 Concepto:
toLocaleString('es-ES')aplica el formato español a los números. Para números de 4 cifras como 2350 no añade separador de miles (es la norma del español); con 23 500 mostraría23.500. Lo verás a fondo conIntlen el módulo 19.
💡 Consejo: Los getters son ideales para datos derivados (nombre completo, total de un carrito, edad a partir de la fecha de nacimiento): así nunca se quedan desactualizados. Los setters, para validar. La privacidad real (campos
#privados) llega con las clases en el módulo 09.
12. Descriptores de propiedad (breve)
Cada propiedad tiene, además de su valor, unos atributos ocultos que controlan su comportamiento:
| Atributo | Significado | Valor por defecto con obj.x = ... | Con defineProperty |
|---|---|---|---|
value | El valor | El asignado | undefined |
writable | ¿Se puede cambiar? | true | false |
enumerable | ¿Aparece en Object.keys, for...in, spread? | true | false |
configurable | ¿Se puede borrar o redefinir? | true | false |
const usuario = { nombre: 'Ana' };
Object.defineProperty(usuario, 'id', {
value: 'u-001',
writable: false,
enumerable: false,
});
console.log(usuario.id); // u-001
console.log(Object.keys(usuario)); // ['nombre'] (id no es enumerable)
console.log(JSON.stringify(usuario)); // {"nombre":"Ana"}
console.log(Object.getOwnPropertyDescriptor(usuario, 'nombre'));
// { value: 'Ana', writable: true, enumerable: true, configurable: true }
💡 Consejo: En el día a día rara vez usarás
definePropertydirectamente, pero entenderlo te explica por quéObject.freezefunciona (ponewritableyconfigurableafalse) o por qué algunas propiedades no aparecen al recorrer un objeto.
13. JSON
📘 Concepto: JSON (JavaScript Object Notation) es un formato de texto para intercambiar datos. Se parece a un literal de objeto, pero es más estricto: claves siempre entre comillas dobles, sin comas finales, sin comentarios, sin funciones ni
undefined. Es el idioma que hablan casi todas las APIs web.
13.1. JSON.stringify: de objeto a texto
const libro = {
titulo: 'Rayuela',
autor: 'Julio Cortázar',
paginas: 600,
disponible: true,
etiquetas: ['novela', 'clásico'],
notas: undefined, // se omite
prestar() {}, // las funciones se omiten
publicado: new Date(Date.UTC(1963, 5, 28)), // se convierte a texto ISO
};
console.log(JSON.stringify(libro));
// {"titulo":"Rayuela","autor":"Julio Cortázar","paginas":600,"disponible":true,"etiquetas":["novela","clásico"],"publicado":"1963-06-28T00:00:00.000Z"}
// Tercer argumento: sangría para hacerlo legible
console.log(JSON.stringify({ a: 1, b: [1, 2] }, null, 2));
// {
// "a": 1,
// "b": [
// 1,
// 2
// ]
// }
// Segundo argumento (replacer) como array: lista blanca de claves
console.log(JSON.stringify(libro, ['titulo', 'paginas'])); // {"titulo":"Rayuela","paginas":600}
// Replacer como función: transformar u ocultar valores
const usuario = { nombre: 'Ana', password: '1234', edad: 30 };
const seguro = JSON.stringify(usuario, (clave, valor) => (clave === 'password' ? undefined : valor));
console.log(seguro); // {"nombre":"Ana","edad":30}
💡 Consejo: Si un objeto tiene un método
toJSON(),JSON.stringifyusa lo que devuelva. Así funcionan las fechas:Date.prototype.toJSONdevuelve el texto ISO.
13.2. JSON.parse: de texto a objeto
const texto = '{"producto":"Silla","precio":45.5,"fechaAlta":"2026-09-15T08:30:00.000Z"}';
const datos = JSON.parse(texto);
console.log(datos.precio); // 45.5
console.log(typeof datos.fechaAlta); // string (JSON no tiene tipo fecha)
// Reviver: segundo argumento que transforma cada valor al leerlo
const conFecha = JSON.parse(texto, (clave, valor) => (clave.startsWith('fecha') ? new Date(valor) : valor));
console.log(conFecha.fechaAlta instanceof Date); // true
// JSON inválido: lanza SyntaxError (¡captúralo si el texto viene de fuera!)
try {
JSON.parse("{'producto': 'Silla'}"); // comillas simples: no es JSON válido
} catch (error) {
console.log(error.name); // SyntaxError
}
13.3. Novedad ES2026: acceso al texto fuente y JSON.rawJSON
JSON no tiene límite de dígitos, pero los números de JavaScript (Number) solo representan enteros exactos hasta Number.MAX_SAFE_INTEGER (9 007 199 254 740 991). Un identificador de 64 bits que llega de una API se corrompe al parsearlo:
const respuesta = '{"id": 12345678901234567890, "nombre": "pedido"}';
console.log(JSON.parse(respuesta).id); // 12345678901234567000 ← ¡ha perdido precisión!
ES2026 resuelve este problema en los dos sentidos:
- Al leer: el reviver recibe un tercer argumento,
context, cuya propiedadsourcecontiene el texto original del valor (solo para valores primitivos). - Al escribir:
JSON.rawJSON(texto)crea un valor queJSON.stringifyinserta tal cual, sin convertirlo.
const respuesta = '{"id": 12345678901234567890, "nombre": "pedido"}';
// Leer: convertir a BigInt los enteros que no caben en Number
const pedido = JSON.parse(respuesta, (clave, valor, context) => {
if (typeof valor === 'number' && !Number.isSafeInteger(valor) && Number.isInteger(valor)) {
return BigInt(context.source); // usamos el texto original, no el número ya redondeado
}
return valor;
});
console.log(pedido.id); // 12345678901234567890n
console.log(typeof pedido.id); // bigint
// Escribir: JSON.stringify no sabe serializar BigInt...
try {
JSON.stringify(pedido);
} catch (error) {
console.log(error.message); // Do not know how to serialize a BigInt
}
// ...pero con un replacer y JSON.rawJSON lo escribimos como número JSON exacto
const json = JSON.stringify(pedido, (clave, valor) =>
typeof valor === 'bigint' ? JSON.rawJSON(valor.toString()) : valor,
);
console.log(json); // {"id":12345678901234567890,"nombre":"pedido"}
⚠️ Importante:
JSON.rawJSONsolo acepta texto que sea un valor primitivo JSON válido (un número, una cadena entre comillas,true,falseonull); no admite objetos ni arrays. Estas funciones están soportadas en Chrome, Edge, Firefox, Safari y Node.js modernos.
14. Datos anidados de una API: ?. y ??
Las respuestas de las APIs suelen ser objetos profundos donde algunos campos pueden faltar. Combinando desestructuración, ?. y ?? escribes código robusto:
// Simulamos la respuesta (en el módulo 17 la obtendrás con fetch)
const respuestaApi = {
usuario: {
id: 42,
perfil: {
nombre: 'Irene',
redes: { github: 'irene-dev' },
},
preferencias: null,
pedidos: [{ id: 'A1', total: 59.9, envio: { estado: 'entregado' } }, { id: 'A2', total: 12 }],
},
};
const { usuario } = respuestaApi;
const nombre = usuario?.perfil?.nombre ?? 'Anónimo';
const twitter = usuario?.perfil?.redes?.twitter ?? 'sin cuenta';
const idioma = usuario?.preferencias?.idioma ?? 'es';
const ultimoEstado = usuario?.pedidos?.at(-1)?.envio?.estado ?? 'pendiente';
const totalPedidos = usuario?.pedidos?.reduce((s, p) => s + p.total, 0) ?? 0;
const notificar = usuario?.preferencias?.notificar?.() ?? false; // ?.() llama solo si existe
console.log(nombre, twitter, idioma); // Irene sin cuenta es
console.log(ultimoEstado); // pendiente
console.log(totalPedidos.toFixed(2)); // 71.90
console.log(notificar); // false
⚠️ Importante: Usa
??y no||para los valores por defecto:||reemplaza también0,''yfalse, que pueden ser valores legítimos. Constock || 10, un stock de0se convertiría en10.
💡 Consejo: No abuses de
?.. Si un dato debe existir (por ejemplo, elidde un usuario), es mejor que el código falle pronto y con claridad (módulo 10) a que siga funcionando conundefinedy falle más tarde en un lugar inesperado.
15. Ejemplos reales
15.1. Configuración con valores por defecto
const CONFIG_POR_DEFECTO = Object.freeze({
idioma: 'es',
tema: 'claro',
paginacion: 20,
notificaciones: { email: true, push: false },
});
function crearConfig(opcionesUsuario = {}) {
return {
...CONFIG_POR_DEFECTO,
...opcionesUsuario,
// El spread es superficial: fusionamos a mano el nivel anidado
notificaciones: {
...CONFIG_POR_DEFECTO.notificaciones,
...opcionesUsuario.notificaciones,
},
};
}
console.log(crearConfig({ tema: 'oscuro', notificaciones: { push: true } }));
// { idioma: 'es', tema: 'oscuro', paginacion: 20, notificaciones: { email: true, push: true } }
console.log(crearConfig().paginacion); // 20
⚠️ Importante: Sin la fusión manual de
notificaciones,{ ...defecto, ...usuario }sustituiría el objeto anidado completo y perderíasemail: true. Es uno de los errores más frecuentes al fusionar configuraciones.
15.2. Agenda de contactos
function crearAgenda() {
const contactos = {}; // clave: id, valor: contacto
let siguienteId = 1;
return {
anadir({ nombre, telefono, email = null, etiquetas = [] }) {
const id = `c${siguienteId++}`;
contactos[id] = { id, nombre, telefono, email, etiquetas };
return id;
},
actualizar(id, cambios) {
if (!Object.hasOwn(contactos, id)) return false;
contactos[id] = { ...contactos[id], ...cambios, id }; // el id no se puede cambiar
return true;
},
eliminar(id) {
return delete contactos[id];
},
buscar(texto) {
const t = texto.toLowerCase();
return Object.values(contactos).filter(({ nombre }) => nombre.toLowerCase().includes(t));
},
porEtiqueta() {
const todos = Object.values(contactos);
const pares = todos.flatMap((c) => c.etiquetas.map((e) => [e, c.nombre]));
return Object.groupBy(pares, ([etiqueta]) => etiqueta);
},
exportar() {
return JSON.stringify(Object.values(contactos), null, 2);
},
};
}
const agenda = crearAgenda();
const idAna = agenda.anadir({ nombre: 'Ana López', telefono: '600111222', etiquetas: ['trabajo'] });
agenda.anadir({ nombre: 'Luis Pérez', telefono: '600333444', etiquetas: ['familia', 'trabajo'] });
agenda.anadir({ nombre: 'Anabel Ríos', telefono: '600555666', email: 'anabel@correo.es' });
agenda.actualizar(idAna, { email: 'ana@empresa.es', id: 'hackeado' });
console.log(agenda.buscar('ana').map((c) => `${c.nombre} <${c.email ?? 'sin email'}>`));
// ['Ana López <ana@empresa.es>', 'Anabel Ríos <anabel@correo.es>']
const grupos = agenda.porEtiqueta();
for (const [etiqueta, pares] of Object.entries(grupos)) {
console.log(`${etiqueta}: ${pares.map(([, nombre]) => nombre).join(', ')}`);
}
// trabajo: Ana López, Luis Pérez
// familia: Luis Pérez
console.log(JSON.parse(agenda.exportar()).length); // 3
📘 Concepto: La agenda usa un closure (módulo 05):
contactosysiguienteIdsolo son accesibles a través de los métodos devueltos. Es una forma de encapsulación sin clases.
16. Ejercicios
Ejercicio 1: Ficha de producto
Crea un objeto producto con nombre, precio, stock y categoría. Añade un método vender(unidades) que reste stock (y lance un Error si no hay suficiente), y un getter disponible que devuelva true si el stock es mayor que 0. Prueba a pasar producto.vender como callback a setTimeout y arregla el problema de this con bind.
Ejercicio 2: Contador de caracteres
Escribe contarLetras(texto) que devuelva un objeto con cuántas veces aparece cada letra (ignora espacios y mayúsculas). Después, usando Object.entries, toSorted y Object.fromEntries, devuelve solo las 3 letras más frecuentes.
Ejercicio 3: Invertir un objeto
Escribe invertir(obj) que intercambie claves y valores: invertir({ a: 'x', b: 'y' }) → { x: 'a', y: 'b' }. Hazlo en una línea con Object.entries, map y Object.fromEntries.
Ejercicio 4: Opciones con desestructuración
Escribe formatearPrecio(cantidad, { moneda = '€', decimales = 2, separador = ',' } = {}) que devuelva, por ejemplo, '12,50 €'. Llámala con distintas combinaciones de opciones y sin opciones.
Ejercicio 5: Copias y referencias
Crea un objeto alumno con un array notas y un objeto tutor. Haz una copia con spread y otra con structuredClone. Modifica en cada copia una nota y el nombre del tutor, y explica con comentarios qué cambios afectan al original y por qué.
Ejercicio 6: Fusión profunda
Generaliza el ejemplo de la sección 15.1: escribe fusionarProfundo(destino, origen) que devuelva un objeto nuevo fusionando recursivamente todos los niveles de objetos (los arrays se sustituyen, no se fusionan). Pruébala con una configuración de tres niveles.
Ejercicio 7: JSON con fechas y BigInt
Dado el texto '{"id": 9007199254740993, "creado": "2026-10-01T12:00:00Z", "total": 99.5}', parsea el JSON de forma que id sea un BigInt exacto (usa context.source) y creado un Date. Después vuelve a serializarlo con JSON.rawJSON comprobando que el id sale idéntico al original.
Ejercicio 8: Normalizar una respuesta de API
Dado un array de usuarios de una API en el que algunos no tienen direccion, otros tienen direccion sin ciudad y otros no tienen telefonos, genera un array de objetos planos { id, nombre, ciudad, telefono } usando ?. y ?? con valores por defecto ('Desconocida', '—').
Resumen
| Concepto | Sintaxis |
|---|---|
| Literal y shorthand | { nombre, edad, saludar() {} } |
| Acceso | obj.prop, obj['prop'], obj[variable] |
| Propiedad computada | { [clave]: valor } |
| Borrar | delete obj.prop |
| Comprobar existencia | Object.hasOwn(obj, 'prop'), 'prop' in obj |
| Acceso seguro | obj?.a?.b, obj.metodo?.(), valor ?? porDefecto |
this | El objeto a la izquierda del punto al llamar; call, apply, bind |
| Recorrer | Object.keys/values/entries, for...of, for...in + hasOwn |
| Reconstruir | Object.fromEntries(pares) |
| Desestructurar | const { a, b: otro, c = 0, d: { e }, ...resto } = obj |
| Spread / fusión | { ...a, ...b }, Object.assign(destino, origen) |
| Copia profunda | structuredClone(obj) |
| Inmutabilidad | Object.freeze, Object.seal, Object.preventExtensions |
| Getters / setters | get total() {}, set total(v) {} |
| Descriptores | Object.defineProperty(obj, 'p', { value, writable, enumerable, configurable }) |
| JSON | JSON.stringify(obj, replacer, 2), JSON.parse(texto, reviver) |
| JSON ES2026 | Reviver (clave, valor, context) => context.source, JSON.rawJSON(texto) |