Prometimos que el alivio llegaría en la primera página, así que empecemos por ahí. Durante todo el Módulo 3 has arrastrado seis arrays paralelos —titulos[i], responsables[i], prioridades[i], estados[i], horas[i], fechasLimite[i]— y una función, describirTarea, que necesitaba ocho argumentos para pintar una sola línea. Cada vez que querías filtrar, filtrabas índices en lugar de tareas. Cada vez que añadías un campo al modelo, tocabas todas las funciones. En esta lección eso se acaba: aprenderás qué es un objeto en JavaScript, cómo se crea, cómo se leen y modifican sus propiedades, y usarás ese conocimiento para escribir por fin el backlog real de Nómada Tareas como un array de objetos, con la firma limpia describirTarea(tarea, hoy). Es el cambio más importante del curso hasta ahora, porque a partir de aquí el código deja de simular el modelo de datos y pasa a contenerlo.

Contenido

  1. El problema: seis arrays que hay que mantener alineados
  2. Qué es un objeto: literales, propiedades y valores
  3. Acceso con punto y acceso con corchetes
  4. Añadir, modificar y borrar propiedades
  5. Propiedades que no existen: undefined y el encadenamiento opcional
  6. Comprobar si una propiedad existe
  7. El backlog real: un array de objetos
  8. La recompensa: describirTarea(tarea, hoy)
  9. Objetos anidados: la tarea con sus subtareas
  10. Nombres de propiedad computados
  11. Recorrer un objeto: for...in y Object.keys/values/entries
  12. Objetos como diccionario de consulta
  13. Una mención breve a Map y Set
  14. Errores Comunes y Consejos
  15. Ejercicios
  16. Conclusión

  1. El problema: seis arrays que hay que mantener alineados

Antes de celebrar la solución, mira el problema con toda su crudeza. Así estaba el backlog al final del Módulo 3:

const titulos      = ['Rediseñar la sala polivalente', 'Cartelería del taller de serigrafía', /* ... */];
const responsables = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const prioridades  = ['alta', 'media', 'alta', 'baja', 'media', 'alta'];
const estados      = ['en-curso', 'pendiente', 'pendiente', 'hecha', 'en-curso', 'pendiente'];
const horas        = [12, 6, 14, 3, 8, 5];
const fechasLimite = ['2026-09-30', '2026-10-15', '2026-10-02', '2026-09-12', '2026-11-05', '2026-09-05'];

Este diseño tiene tres defectos graves, y conviene nombrarlos:

Defecto Qué pasa en la práctica
Fragilidad Si borras la tarea 3 de titulos y olvidas borrarla de horas, a partir de ese índice todo el backlog está desalineado y el programa no da ningún error: simplemente miente
Firmas enormes describirTarea(id, titulo, responsable, prioridad, estado, horasEstimadas, fechaLimite, hoy) — ocho parámetros posicionales que hay que recordar en orden
No hay "la tarea" No existe ningún valor en el programa que sea una tarea. Solo existe el número 3, y el acuerdo tácito de que el índice 3 significa lo mismo en seis sitios

Un objeto resuelve los tres de una vez: agrupa datos relacionados bajo un mismo valor, con nombres en lugar de posiciones.

  1. Qué es un objeto: literales, propiedades y valores

Un objeto es una colección de pares clave → valor. La forma de crearlo es el literal de objeto: llaves {} con las parejas dentro.

const tarea = {
  id: 1,
  titulo: 'Rediseñar la sala polivalente',
  responsable: 'Iván',
  prioridad: 'alta',
  estado: 'en-curso',
  etiquetas: ['espacio', 'diseño'],
  horasEstimadas: 12,
  fechaLimite: '2026-09-30',
  revisor: 'Marta'
};

Vocabulario preciso, porque lo usarás en todo el módulo:

  • Propiedad: cada pareja completa, por ejemplo titulo: 'Rediseñar la sala polivalente'.
  • Clave (o nombre de propiedad): la parte izquierda, titulo. Siempre es un string internamente (o un Symbol, que no veremos aquí). Aunque escribas { 1: 'uno' }, la clave real es '1'.
  • Valor: la parte derecha. Puede ser cualquier cosa: un número, un string, null, un array (etiquetas), otro objeto, o incluso una función (eso son los métodos, y llegan en la lección siguiente).

Tres detalles de sintaxis que ahorran disgustos:

const ejemplo = {
  titulo: 'Sin comillas si es un identificador válido',
  'fecha-limite': 'Con comillas si lleva guiones, espacios o empieza por dígito',
  estado: 'pendiente',      // la coma final (trailing comma) está permitida
};

Las claves no llevan comillas si son identificadores válidos de JavaScript (letras, dígitos que no van al principio, _ y $). Si la clave tiene un guion, un espacio o un acento problemático, hay que entrecomillarla. En Nómada Tareas usamos siempre claves en camelCase sin comillas: horasEstimadas, fechaLimite.

Y recuerda lo que ya sabes de 01-05: typeof de un objeto es 'object', y los objetos son valores de referencia. Dos objetos con el mismo contenido no son iguales:

console.log(typeof tarea);                 // 'object'
console.log({ id: 1 } === { id: 1 });      // false  ← son dos objetos distintos

  1. Acceso con punto y acceso con corchetes

Hay dos formas de leer una propiedad, y no son intercambiables.

// Notación de punto: la clave se escribe literalmente en el código
console.log(tarea.titulo);        // 'Rediseñar la sala polivalente'
console.log(tarea.horasEstimadas); // 12

// Notación de corchetes: la clave es una EXPRESIÓN que se evalúa a un string
console.log(tarea['titulo']);     // 'Rediseñar la sala polivalente'

const campo = 'responsable';
console.log(tarea[campo]);        // 'Iván'   ← imposible con el punto
console.log(tarea.campo);         // undefined ← busca la clave literal "campo"

Esa última línea es el error clásico: tarea.campo busca una propiedad llamada campo, no el contenido de la variable campo. Cuándo hace falta cada notación:

Situación Notación Ejemplo
Sabes el nombre al escribir el código Punto (preferida: más corta y legible) tarea.estado
El nombre está en una variable Corchetes tarea[campo]
El nombre se calcula Corchetes tarea['horas' + 'Estimadas']
El nombre no es un identificador válido Corchetes config['modo-oscuro']
El nombre es un número Corchetes pesos[3]

Un caso práctico donde los corchetes son imprescindibles: una función genérica que ordena o compara por el campo que le digas.

function valorDelCampo(tarea, campo) {
  return tarea[campo];      // con punto sería siempre la propiedad "campo"
}

console.log(valorDelCampo(tarea, 'prioridad'));  // 'alta'
console.log(valorDelCampo(tarea, 'revisor'));    // 'Marta'

  1. Añadir, modificar y borrar propiedades

Los objetos declarados con const sí se pueden modificar. Lo que const impide es reasignar la variable, no cambiar el contenido del objeto al que apunta (esto lo viste en 01-05 con primitivos frente a referencias).

const tarea = { id: 1, titulo: 'Rediseñar la sala polivalente', estado: 'pendiente' };

// Modificar: la clave ya existe
tarea.estado = 'en-curso';

// Añadir: la clave no existía, se crea sobre la marcha
tarea.responsable = 'Iván';
tarea['horasEstimadas'] = 12;

console.log(tarea);
// { id: 1, titulo: 'Rediseñar la sala polivalente', estado: 'en-curso',
//   responsable: 'Iván', horasEstimadas: 12 }

// Borrar: el operador delete elimina la propiedad entera
delete tarea.horasEstimadas;
console.log(tarea.horasEstimadas);   // undefined

// Reasignar la variable SÍ falla
// tarea = { id: 2 };   // ✗ TypeError: Assignment to constant variable

Sobre delete, dos matices importantes:

  • delete no es lo mismo que poner undefined. Tras delete tarea.revisor, la clave revisor ya no existe en el objeto. Tras tarea.revisor = undefined, la clave existe y su valor es undefined. Leyendo con el punto no notas la diferencia, pero 'revisor' in tarea sí (apartado 6), y también la notan Object.keys y JSON.stringify (lección 04-08).
  • En Nómada Tareas casi nunca usamos delete. El modelo dice que revisor es «string o null»: un revisor ausente se representa con null, que es un valor explícito, no borrando la clave. Borrar campos del modelo canónico rompe las funciones que esperan encontrarlos.

  1. Propiedades que no existen: undefined y el encadenamiento opcional

Leer una propiedad inexistente no da error: devuelve undefined.

const tarea = { id: 1, titulo: 'Rediseñar la sala polivalente' };

console.log(tarea.responsable);   // undefined   ← no existe, pero no falla
console.log(tarea.titluo);        // undefined   ← ¡una errata también devuelve undefined!

Esa segunda línea explica por qué las erratas en nombres de propiedad son tan traicioneras: JavaScript no te avisa, simplemente te da undefined y el fallo aparece tres funciones más allá. (En 08-02 verás cómo un linter y las anotaciones de tipo detectan esto antes de ejecutar.)

Lo que revienta es intentar leer una propiedad de undefined:

const tarea = { id: 1, titulo: 'Rediseñar la sala polivalente' };

console.log(tarea.revision.autor);
// ✗ TypeError: Cannot read properties of undefined (reading 'autor')

Aquí es donde vuelve el encadenamiento opcional ?. que conociste en 01-06. Si lo que hay a la izquierda es null o undefined, la expresión entera se detiene y devuelve undefined en lugar de lanzar el error:

console.log(tarea.revision?.autor);          // undefined  ← sin TypeError
console.log(tarea.revision?.autor ?? 'sin revisar');   // 'sin revisar'

Combinado con ?? (fusión de nulos), tienes la pareja perfecta para leer datos que puede que no estén: ?. evita el error y ?? pone el valor por defecto. Un ejemplo directo del proyecto, donde revisor puede ser null:

function nombreDelRevisor(tarea) {
  return tarea.revisor ?? 'pendiente de asignar';
}

console.log(nombreDelRevisor({ revisor: 'Marta' }));   // 'Marta'
console.log(nombreDelRevisor({ revisor: null }));      // 'pendiente de asignar'

Cuidado con abusar de ?.. Si escribes tarea?.titulo?.length en todas partes, estás tapando el hecho de que no sabes qué datos recibes. Úsalo donde el dato es legítimamente opcional (revisor, un campo que aún no existe), no como red de seguridad universal.

  1. Comprobar si una propiedad existe

Hay cuatro maneras de preguntar «¿tiene este objeto esta propiedad?», y no responden exactamente a la misma pregunta.

Forma Devuelve true cuando… Con { revisor: undefined } Con propiedades heredadas
tarea.revisor !== undefined El valor no es undefined false Sí las ve
'revisor' in tarea La clave existe, con el valor que sea true Sí las ve
tarea.hasOwnProperty('revisor') La clave es propia del objeto true No
Object.hasOwn(tarea, 'revisor') La clave es propia (versión moderna, recomendada) true No
const tarea = { id: 1, titulo: 'Presupuesto de la carpintería', revisor: undefined };

console.log(tarea.revisor !== undefined);        // false  ← el valor es undefined
console.log('revisor' in tarea);                 // true   ← pero la clave sí está
console.log(Object.hasOwn(tarea, 'revisor'));    // true
console.log(Object.hasOwn(tarea, 'prioridad'));  // false

¿Cuál usar? La regla práctica:

  • Object.hasOwn(obj, clave) cuando quieres saber si la clave está presente. Es la forma moderna y la que deberías escribir por defecto.
  • obj.clave !== undefined (o directamente if (obj.clave), con cuidado con los falsy de 01-07) cuando lo que te importa es el valor.
  • in funciona, pero también encuentra propiedades heredadas del prototipo (Módulo 5), lo que a veces sorprende.
  • hasOwnProperty es el veterano; Object.hasOwn existe precisamente para sustituirlo con menos trampas.

Ejemplo real: validar que un objeto recibido tiene todos los campos obligatorios del modelo.

const CAMPOS_OBLIGATORIOS = ['id', 'titulo', 'responsable', 'prioridad',
                             'estado', 'etiquetas', 'horasEstimadas', 'fechaLimite'];

function camposQueFaltan(tarea) {
  const faltan = [];
  for (const campo of CAMPOS_OBLIGATORIOS) {
    if (!Object.hasOwn(tarea, campo)) faltan.push(campo);
  }
  return faltan;
}

console.log(camposQueFaltan({ id: 7, titulo: 'Revisar la guillotina' }));
// [ 'responsable', 'prioridad', 'estado', 'etiquetas', 'horasEstimadas', 'fechaLimite' ]

Fíjate en que revisor no está en la lista: es opcional, y su ausencia se representa con null.

  1. El backlog real: un array de objetos

Ha llegado el momento. Este bloque de código es la fuente de verdad del resto del módulo; lo vas a ver reaparecer en las siete lecciones siguientes.

'use strict';

const HOY = '2026-09-20';

const backlog = [
  {
    id: 1,
    titulo: 'Rediseñar la sala polivalente',
    responsable: 'Iván',
    prioridad: 'alta',
    estado: 'en-curso',
    etiquetas: ['espacio', 'diseño'],
    horasEstimadas: 12,
    fechaLimite: '2026-09-30',
    revisor: 'Marta'
  },
  {
    id: 2,
    titulo: 'Cartelería del taller de serigrafía',
    responsable: 'Marta',
    prioridad: 'media',
    estado: 'pendiente',
    etiquetas: ['serigrafía', 'comunicación'],
    horasEstimadas: 6,
    fechaLimite: '2026-10-15',
    revisor: null
  },
  {
    id: 3,
    titulo: 'Actualizar la web de reservas',
    responsable: 'Lucía',
    prioridad: 'alta',
    estado: 'pendiente',
    etiquetas: ['web', 'reservas'],
    horasEstimadas: 14,
    fechaLimite: '2026-10-02',
    revisor: 'Iván'
  },
  {
    id: 4,
    titulo: 'Inventario de tintas de serigrafía',
    responsable: 'Marta',
    prioridad: 'baja',
    estado: 'hecha',
    etiquetas: ['serigrafía', 'almacén'],
    horasEstimadas: 3,
    fechaLimite: '2026-09-12',
    revisor: null
  },
  {
    id: 5,
    titulo: 'Guía de encuadernación para residentes',
    responsable: 'Iván',
    prioridad: 'media',
    estado: 'en-curso',
    etiquetas: ['encuadernación', 'documentación'],
    horasEstimadas: 8,
    fechaLimite: '2026-11-05',
    revisor: 'Lucía'
  },
  {
    id: 6,
    titulo: 'Presupuesto de la carpintería',
    responsable: 'Iván',
    prioridad: 'alta',
    estado: 'pendiente',
    etiquetas: ['carpintería', 'compras'],
    horasEstimadas: 5,
    fechaLimite: '2026-09-05',
    revisor: 'Marta'
  }
];

Los datos son exactamente los mismos de siempre —48 h totales, 45 h abiertas, Iván con tres tareas y 25 h, «Presupuesto de la carpintería» vencida— pero ahora cada tarea es una unidad. Compruébalo:

console.log(backlog.length);            // 6
console.log(backlog[0].titulo);         // 'Rediseñar la sala polivalente'
console.log(backlog[5].fechaLimite);    // '2026-09-05'
console.log(backlog[2].etiquetas[0]);   // 'web'

Lee despacio backlog[2].etiquetas[0]: del array backlog coge el elemento en la posición 2 (un objeto), de ese objeto lee la propiedad etiquetas (un array) y de ese array coge el elemento 0 (un string). El acceso se encadena de izquierda a derecha, paso a paso.

flowchart TD
    A["backlog<br/>(array de 6 elementos)"] --> B0["[0] objeto tarea"]
    A --> B1["[1] objeto tarea"]
    A --> B2["[2] objeto tarea"]
    A --> B5["[5] objeto tarea"]
    B2 --> C1["id: 3"]
    B2 --> C2["titulo: 'Actualizar la web de reservas'"]
    B2 --> C3["etiquetas: ['web', 'reservas']"]
    B2 --> C4["revisor: 'Iván'"]

Y ahora la comprobación que importa: borrar una tarea ya no puede desalinear nada.

// Con arrays paralelos había que borrar el índice 3 en SEIS arrays.
// Ahora la tarea entera desaparece de un golpe, y nada se descoloca.

  1. La recompensa: describirTarea(tarea, hoy)

Recuperemos las funciones del Módulo 3 y reescribámoslas con el modelo real.

function pesoDePrioridad(prioridad) {
  if (prioridad === 'alta') return 3;
  if (prioridad === 'media') return 2;
  if (prioridad === 'baja') return 1;
  return 0;
}

// R10: vencida = fecha límite pasada y la tarea no está terminada.
function estaVencida(tarea, hoy) {
  return tarea.fechaLimite < hoy && tarea.estado !== 'hecha';
}

function marcaDeEstado(estado) {
  if (estado === 'hecha') return '✓';
  if (estado === 'en-curso') return '▸';
  return '○';
}

function describirTarea(tarea, hoy) {
  const marca = marcaDeEstado(tarea.estado);
  const aviso = estaVencida(tarea, hoy) ? ' ⚠ VENCIDA' : '';
  const revision = tarea.revisor ?? 'sin revisor';
  return `${marca} [${tarea.id}] ${tarea.titulo} · ${tarea.responsable} · ` +
         `${tarea.prioridad} · ${tarea.horasEstimadas} h · rev: ${revision}${aviso}`;
}

console.log(describirTarea(backlog[0], HOY));
// ▸ [1] Rediseñar la sala polivalente · Iván · alta · 12 h · rev: Marta

console.log(describirTarea(backlog[5], HOY));
// ○ [6] Presupuesto de la carpintería · Iván · alta · 5 h · rev: Marta ⚠ VENCIDA

console.log(describirTarea(backlog[3], HOY));
// ✓ [4] Inventario de tintas de serigrafía · Marta · baja · 3 h · sin revisor

Compara las dos firmas de describirTarea:

Módulo 3 Ahora
Firma describirTarea(id, titulo, responsable, prioridad, estado, horasEstimadas, fechaLimite, hoy) describirTarea(tarea, hoy)
Llamada describirTarea(ids[i], titulos[i], responsables[i], prioridades[i], estados[i], horas[i], fechasLimite[i], HOY) describirTarea(backlog[i], HOY)
Añadir el campo revisor Cambiar la firma y todas las llamadas No cambia nada: el campo ya viaja dentro
Riesgo de equivocar el orden Alto: siete strings y números seguidos Ninguno

Este es el beneficio real de los objetos: los datos que van juntos viajan juntos. Y fíjate en el efecto colateral en estaVencida(tarea, hoy): pasó de tres parámetros a dos, y ya no puede recibir la fecha de una tarea con el estado de otra.

  1. Objetos anidados: la tarea con sus subtareas

En 03-07 usaste, a regañadientes, un objeto con subtareas anidadas. Ahora ya puedes leerlo con propiedad:

const tareaRediseno = {
  id: 1,
  titulo: 'Rediseñar la sala polivalente',
  responsable: 'Iván',
  estado: 'en-curso',
  horasEstimadas: 0,
  subtareas: [
    { id: 11, titulo: 'Medir y levantar el plano', responsable: 'Iván',
      estado: 'hecha', horasEstimadas: 3, subtareas: [] },
    { id: 12, titulo: 'Elegir el mobiliario', responsable: 'Marta',
      estado: 'en-curso', horasEstimadas: 0,
      subtareas: [
        { id: 121, titulo: 'Pedir presupuestos', responsable: 'Marta',
          estado: 'hecha', horasEstimadas: 2, subtareas: [] },
        { id: 122, titulo: 'Visitar a dos proveedores', responsable: 'Marta',
          estado: 'pendiente', horasEstimadas: 2, subtareas: [] }
      ]
    },
    { id: 13, titulo: 'Pintar y montar', responsable: 'Iván',
      estado: 'pendiente', horasEstimadas: 5, subtareas: [] }
  ]
};

console.log(tareaRediseno.subtareas[1].subtareas[0].titulo);   // 'Pedir presupuestos'
console.log(tareaRediseno.subtareas[0].subtareas.length);      // 0
console.log(tareaRediseno.subtareas[9]?.titulo);               // undefined (sin error)

La última línea muestra por qué ?. es tan útil con estructuras anidadas: subtareas[9] no existe, y sin ?. obtendrías un TypeError. Un valor puede anidarse tan profundo como quieras: un objeto dentro de un array dentro de un objeto dentro de un array. El acceso siempre se lee de izquierda a derecha, un paso cada vez.

  1. Nombres de propiedad computados

A veces la clave de una propiedad no se conoce hasta que el programa corre. La sintaxis de nombre de propiedad computado permite calcularla dentro del propio literal, poniéndola entre corchetes:

const campo = 'estado';
const valor = 'en-curso';

const filtro = { [campo]: valor };
console.log(filtro);          // { estado: 'en-curso' }

// Sin corchetes, la clave sería literalmente "campo"
const malFiltro = { campo: valor };
console.log(malFiltro);       // { campo: 'en-curso' }   ✗

Se puede usar cualquier expresión que produzca un string:

const persona = 'Iván';
const contador = {
  [`tareas_de_${persona}`]: 3,
  [`horas_de_${persona}`]: 25
};
console.log(contador);        // { tareas_de_Iván: 3, horas_de_Iván: 25 }

Un uso práctico: una función que construye un criterio de búsqueda a partir del campo que el usuario elija en el futuro panel de Nómada Tareas.

function criterio(campo, valor) {
  return { [campo]: valor };
}

console.log(criterio('responsable', 'Lucía'));   // { responsable: 'Lucía' }
console.log(criterio('prioridad', 'alta'));      // { prioridad: 'alta' }

  1. Recorrer un objeto: for...in y Object.keys/values/entries

Un array se recorre por posiciones; un objeto se recorre por claves. Hay dos caminos.

for...in recorre las claves (siempre como strings):

const tarea = backlog[0];

for (const clave in tarea) {
  console.log(`${clave}: ${tarea[clave]}`);
}
// id: 1
// titulo: Rediseñar la sala polivalente
// responsable: Iván
// prioridad: alta
// estado: en-curso
// etiquetas: espacio,diseño
// horasEstimadas: 12
// fechaLimite: 2026-09-30
// revisor: Marta

Fíjate en el acceso: dentro del bucle hay que usar corchetes, tarea[clave], porque la clave está en una variable. tarea.clave daría undefined nueve veces.

Los tres métodos de Object convierten el objeto en arrays, que ya sabes manejar:

console.log(Object.keys(tarea));
// [ 'id', 'titulo', 'responsable', 'prioridad', 'estado',
//   'etiquetas', 'horasEstimadas', 'fechaLimite', 'revisor' ]

console.log(Object.values(tarea));
// [ 1, 'Rediseñar la sala polivalente', 'Iván', 'alta', 'en-curso',
//   [ 'espacio', 'diseño' ], 12, '2026-09-30', 'Marta' ]

console.log(Object.entries(tarea));
// [ [ 'id', 1 ], [ 'titulo', 'Rediseñar la sala polivalente' ], ... ]
//   ↑ un array de pares [clave, valor]

Comparación práctica:

Forma Qué te da Ventaja Inconveniente
for...in Una clave por vuelta Sintaxis directa También recorre propiedades heredadas del prototipo (Módulo 5)
Object.keys(o) Array de claves propias Solo propias; puedes usar métodos de array Crea un array intermedio
Object.values(o) Array de valores propios Ideal para sumar o contar Pierdes los nombres
Object.entries(o) Array de pares [clave, valor] Lo tienes todo Sintaxis algo más densa (mejora mucho con desestructuración, 04-06)

En la práctica, Object.entries con un for...of es la opción por defecto, y for...in queda para casos puntuales:

for (const par of Object.entries(tarea)) {
  console.log(`${par[0]} → ${par[1]}`);
}

En 04-06 verás cómo escribir eso mismo como for (const [clave, valor] of Object.entries(tarea)), que es mucho más legible.

Un ejemplo útil: contar cuántos campos del modelo están vacíos.

function camposVacios(tarea) {
  const vacios = [];
  for (const clave of Object.keys(tarea)) {
    const valor = tarea[clave];
    if (valor === null || valor === undefined || valor === '') vacios.push(clave);
  }
  return vacios;
}

console.log(camposVacios(backlog[1]));   // [ 'revisor' ]
console.log(camposVacios(backlog[0]));   // []

  1. Objetos como diccionario de consulta

Hay un segundo uso de los objetos, distinto de «agrupar los campos de una entidad»: usarlos como diccionario o tabla de consulta, donde las claves son datos y no nombres fijos. Ya lo viste de pasada en 02-03 como alternativa al switch:

const PESOS = { alta: 3, media: 2, baja: 1 };

function pesoDePrioridad(prioridad) {
  return PESOS[prioridad] ?? 0;     // corchetes: la clave viene en una variable
}

console.log(pesoDePrioridad('alta'));       // 3
console.log(pesoDePrioridad('urgentí'));    // 0  ← clave desconocida → undefined → ?? 0

Compara las dos versiones de la misma función:

Cadena de if (03-01) Diccionario
Añadir una prioridad nueva Escribir otro if Añadir una línea a PESOS
Los datos están… Mezclados con la lógica Separados, en un solo sitio
Coste de la búsqueda Recorre los if en orden Acceso directo por clave

El patrón se generaliza a cualquier traducción valor → valor:

const MARCAS = { hecha: '✓', 'en-curso': '▸', pendiente: '○' };
const NOMBRES_ESTADO = { pendiente: 'Pendiente', 'en-curso': 'En curso', hecha: 'Terminada' };

function marcaDeEstado(estado) {
  return MARCAS[estado] ?? '?';
}

function nombreDeEstado(estado) {
  return NOMBRES_ESTADO[estado] ?? 'Desconocido';
}

console.log(`${marcaDeEstado('en-curso')} ${nombreDeEstado('en-curso')}`);   // ▸ En curso

Observa que 'en-curso' necesita comillas como clave, porque el guion no es válido en un identificador. Al leerla, MARCAS['en-curso'] con corchetes; MARCAS.en-curso sería un error de sintaxis (JavaScript lo leería como una resta).

Y un tercer uso, el índice por identificador, que evita recorrer el backlog entero cada vez que buscas una tarea:

const porId = {};
for (const tarea of backlog) {
  porId[tarea.id] = tarea;      // la clave numérica se convierte en el string '1', '2'...
}

console.log(porId[3].titulo);   // 'Actualizar la web de reservas'
console.log(porId[99]?.titulo); // undefined

En 04-05 construirás este índice de forma mucho más elegante con reduce.

  1. Una mención breve a Map y Set

Los objetos como diccionario tienen dos límites: sus claves solo pueden ser strings (todo lo demás se convierte a string) y no llevan la cuenta de cuántas entradas tienen. Para esos casos JavaScript ofrece dos estructuras dedicadas:

  • Map: un diccionario donde la clave puede ser cualquier valor (un número de verdad, un objeto, una función), conserva el orden de inserción y tiene .size.
  • Set: una colección de valores sin duplicados, ideal para deduplicar etiquetas.
const etiquetasUnicas = new Set(['serigrafía', 'compras', 'serigrafía']);
console.log(etiquetasUnicas.size);   // 2

No las desarrollamos aquí: aparecen con detalle en Buscar, Ordenar y Agregar Datos, cuando tengas que agrupar y deduplicar de verdad. Para el 95 % de lo que harás en este curso, un objeto literal es suficiente.

Errores Comunes y Consejos

1. Usar el punto cuando la clave está en una variable.

const campo = 'estado';
console.log(tarea.campo);    // ✗ undefined
console.log(tarea[campo]);   // ✓ 'en-curso'

2. Olvidar las comillas al leer una clave con corchetes.

console.log(tarea[titulo]);    // ✗ ReferenceError: titulo is not defined
console.log(tarea['titulo']);  // ✓

Con corchetes, lo de dentro es una expresión. Sin comillas, JavaScript busca una variable con ese nombre.

3. Creer que const congela el objeto. No lo hace: const protege la variable, no el contenido. Para impedir cambios en el objeto existe Object.freeze, que verás en 04-08.

4. Confundir «no existe» con «vale undefined». Usa Object.hasOwn cuando la pregunta sea sobre la clave; compara con undefined cuando sea sobre el valor.

5. Encadenar accesos sin protección. tarea.subtareas[0].titulo explota si subtareas está vacío. Con tarea.subtareas[0]?.titulo obtienes undefined y sigues vivo.

6. Poner una coma de más entre propiedades. { id: 1,, titulo: 'x' } es un error de sintaxis. La coma final ({ id: 1, }) sí está permitida y es buena práctica: los diffs de Git salen más limpios.

Consejo profesional. Elige un orden fijo para los campos —en Nómada Tareas: id, titulo, responsable, prioridad, estado, etiquetas, horasEstimadas, fechaLimite, revisor— y respétalo en todos los objetos. Leer código es comparar, y comparar es mucho más rápido cuando todo está en el mismo sitio.

Ejercicios

Ejercicio 1 — Fichas del taller. Crea un objeto taller que describa el espacio de Taller Nómada con las propiedades nombre ('Taller Nómada'), plazas (18), zonas (array con 'coworking', 'serigrafía', 'encuadernación', 'carpintería') y coordinadora ('Marta'). Después:

  1. Muestra por consola la segunda zona.
  2. Añade la propiedad abiertoSabados con valor true.
  3. Cambia plazas a 20.
  4. Borra coordinadora y comprueba con Object.hasOwn que ya no está.
  5. Lee una propiedad inexistente telefono sin que el programa falle, mostrando 'sin teléfono' si no existe.

Ejercicio 2 — Diccionario de plazos. Escribe una función diasDeMargen(prioridad) que, usando un objeto de consulta (no if ni switch), devuelva cuántos días de margen tiene una tarea según su prioridad: alta → 3, media → 7, baja → 15. Si la prioridad es desconocida, debe devolver 30. Prueba con 'alta', 'baja' y 'ninguna'.

Ejercicio 3 — Inspector de tareas. Escribe una función inspeccionar(tarea) que reciba una tarea del backlog y devuelva un string multilínea con una línea por campo, en el formato clave: valor, saltándose el campo etiquetas. Después escribe resumenDeCampos(tarea), que devuelva un objeto con tres propiedades: total (número de campos), vacios (número de campos cuyo valor sea null) y nombres (array con los nombres de los campos). Pruébalo con backlog[1].

Soluciones

Ejercicio 1

const taller = {
  nombre: 'Taller Nómada',
  plazas: 18,
  zonas: ['coworking', 'serigrafía', 'encuadernación', 'carpintería'],
  coordinadora: 'Marta'
};

// 1. Segunda zona: el índice 1, porque los arrays empiezan en 0
console.log(taller.zonas[1]);                      // 'serigrafía'

// 2. Añadir una propiedad nueva
taller.abiertoSabados = true;

// 3. Modificar una existente
taller.plazas = 20;

// 4. Borrar y comprobar
delete taller.coordinadora;
console.log(Object.hasOwn(taller, 'coordinadora'));   // false

// 5. Propiedad inexistente sin fallar
console.log(taller.telefono ?? 'sin teléfono');       // 'sin teléfono'

console.log(taller);
// { nombre: 'Taller Nómada', plazas: 20,
//   zonas: [ 'coworking', 'serigrafía', 'encuadernación', 'carpintería' ],
//   abiertoSabados: true }

El error frecuente aquí es el punto 1: taller.zonas[2] devolvería 'encuadernación', la tercera. Recuerda que la posición 1 es el segundo elemento.

Ejercicio 2

const MARGEN_EN_DIAS = { alta: 3, media: 7, baja: 15 };

function diasDeMargen(prioridad) {
  return MARGEN_EN_DIAS[prioridad] ?? 30;
}

console.log(diasDeMargen('alta'));      // 3
console.log(diasDeMargen('baja'));      // 15
console.log(diasDeMargen('ninguna'));   // 30

Detalle importante: aquí ?? es más correcto que ||. Si algún día una prioridad tuviera margen 0, MARGEN_EN_DIAS[p] || 30 devolvería 30 porque 0 es falsy, mientras que ?? solo actúa ante null o undefined. Es exactamente la distinción que estudiaste en 01-06.

Ejercicio 3

function inspeccionar(tarea) {
  const lineas = [];
  for (const clave of Object.keys(tarea)) {
    if (clave === 'etiquetas') continue;
    lineas.push(`${clave}: ${tarea[clave]}`);
  }
  return lineas.join('\n');
}

function resumenDeCampos(tarea) {
  const nombres = Object.keys(tarea);
  let vacios = 0;
  for (const clave of nombres) {
    if (tarea[clave] === null) vacios++;
  }
  return { total: nombres.length, vacios: vacios, nombres: nombres };
}

console.log(inspeccionar(backlog[1]));
// id: 2
// titulo: Cartelería del taller de serigrafía
// responsable: Marta
// prioridad: media
// estado: pendiente
// horasEstimadas: 6
// fechaLimite: 2026-10-15
// revisor: null

console.log(resumenDeCampos(backlog[1]));
// { total: 9, vacios: 1,
//   nombres: [ 'id', 'titulo', 'responsable', 'prioridad', 'estado',
//              'etiquetas', 'horasEstimadas', 'fechaLimite', 'revisor' ] }

Dos observaciones. Primera: resumenDeCampos devuelve un objeto, que es justo lo que recomendaba 03-03 para retornar varios valores con nombre; ahora ya sabes cómo se construye ese objeto. Segunda: el continue dentro del for...of es el mismo de 02-04, y aquí sirve para saltarse el campo etiquetas sin romper el bucle.

Conclusión

Has saldado la deuda. Un objeto es una colección de pares clave → valor que se escribe con un literal {}; sus propiedades se leen con punto cuando conoces el nombre y con corchetes cuando el nombre está en una variable o se calcula. Sabes añadir y modificar propiedades sobre la marcha, borrarlas con delete (y por qué en este proyecto casi nunca conviene), y qué ocurre al leer una que no existe: undefined, sin error, salvo que intentes seguir encadenando —para eso está ?., que ahora entiendes de verdad—. Tienes también la tabla de las cuatro formas de comprobar la existencia de una clave, con Object.hasOwn como opción recomendada.

Y sobre todo tienes el backlog real: seis objetos dentro de un array, con los mismos datos de siempre (48 h totales, 45 abiertas, Iván con 25 h, la carpintería vencida) pero por fin estructurados. Con él has reescrito estaVencida(tarea, hoy) y describirTarea(tarea, hoy), y has comprobado el cambio de escala: de ocho parámetros posicionales a dos, de seis arrays que había que mantener sincronizados a una lista que se borra y se ordena sin miedo. Has visto además los objetos anidados del árbol de subtareas, los nombres de propiedad computados { [clave]: valor }, las cuatro formas de recorrer un objeto (for...in, Object.keys, Object.values, Object.entries) y el uso de los objetos como diccionario de consulta, que convierte cuatro if en una línea.

Pero un objeto puede contener algo más que datos. Si PESOS puede vivir dentro de un objeto, ¿por qué no pesoDePrioridad? ¿Y si el tablero de Taller Nómada guardara, junto a su lista de tareas, las propias operaciones de agregar una tarea o cambiarle el estado? Eso es exactamente lo que veremos en Métodos de Objeto y la Palabra Clave this: funciones guardadas en propiedades, la sintaxis abreviada para escribirlas y esa palabra clave, this, que es a la vez la más útil y la más malentendida de JavaScript. De paso cerrarás otra deuda pendiente del Módulo 3: por qué las funciones flecha se comportan de forma distinta a las normales.

Curso de JavaScript: De Principiante a Avanzado

Módulo 1: Introducción a JavaScript

Módulo 2: Estructuras de Control

Módulo 3: Funciones

Módulo 4: Objetos y Arrays

Módulo 5: Objetos y Funciones Avanzadas

Módulo 6: El Modelo de Objetos del Documento (DOM)

Módulo 7: APIs del Navegador y Temas Avanzados

Módulo 8: Pruebas y Depuración

Módulo 9: Rendimiento y Optimización

Módulo 10: Frameworks y Librerías de JavaScript

Módulo 11: Proyecto Final

© Copyright 2026. Todos los derechos reservados