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.values y Object.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.seal y Object.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] y obj['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, const impide reasignar la variable (producto = {}), pero no impide cambiar sus propiedades. Para eso existe Object.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 valor 0 es falsy y la condición fallaría aunque la propiedad exista. Lo mismo ocurre con '' y false.

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, this es 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, this sería el objeto global (window) y el fallo pasaría desapercibido. Esto pasa también al hacer setTimeout(perro.ladrar, 1000) o boton.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 a this con 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/entries con for...of o con métodos de array: solo devuelven propiedades propias y te dan acceso a map, 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, director no se crea como variable cuando escribes { director: { nombre } }: solo indica el camino. Y si pelicula.director fuera undefined, se lanzaría un TypeError. 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ía TypeError al intentar desestructurar undefined.


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, como isDeepStrictEqual del módulo node:util en 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: structuredClone tampoco conserva el prototipo de las instancias de clase: un objeto creado con new 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.freeze es 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.freeze es 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ía 23.500. Lo verás a fondo con Intl en 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 defineProperty directamente, pero entenderlo te explica por qué Object.freeze funciona (pone writable y configurable a false) 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.stringify usa lo que devuelva. Así funcionan las fechas: Date.prototype.toJSON devuelve 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:

  1. Al leer: el reviver recibe un tercer argumento, context, cuya propiedad source contiene el texto original del valor (solo para valores primitivos).
  2. Al escribir: JSON.rawJSON(texto) crea un valor que JSON.stringify inserta 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.rawJSON solo acepta texto que sea un valor primitivo JSON válido (un número, una cadena entre comillas, true, false o null); 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én 0, '' y false, que pueden ser valores legítimos. Con stock || 10, un stock de 0 se convertiría en 10.

💡 Consejo: No abuses de ?.. Si un dato debe existir (por ejemplo, el id de un usuario), es mejor que el código falle pronto y con claridad (módulo 10) a que siga funcionando con undefined y 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ías email: 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): contactos y siguienteId solo 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)