En las lecciones anteriores han ido apareciendo, sin explicación formal, herramientas como flatMap, el operador ..., la coalescencia nula ?? o las funciones flecha. Es hora de ponerles nombre. Node.js incorpora un motor V8 muy actualizado, lo que significa que puedes usar todo el JavaScript moderno sin transpiladores ni configuración: no hay navegadores antiguos a los que dar soporte en el servidor.
Esta lección no es una referencia del lenguaje —para eso está la documentación—, sino un repaso práctico: cada apartado resuelve un problema real del catálogo de Escena Viva. Si dominas ya estos conceptos, léela como una lista de comprobación; si vienes de un JavaScript más antiguo, aquí está la base de todo el código del curso.
Un aviso previo: async/await y las promesas no se tratan aquí. Son el corazón del Módulo 2 y merecen su propio espacio.
Contenido
const,lety el ámbito de bloque- Funciones flecha y el valor de
this - Desestructuración, parámetros por defecto y propiedades abreviadas
- Propagación y resto: el operador
... - Encadenamiento opcional
?.y coalescencia nula?? - Plantillas de cadena
- Métodos de array esenciales aplicados al catálogo
- Objetos:
Object.keys,valuesyentries MapySet: cuándo usar cada uno- Clases, campos y métodos privados
- JSON:
parseystringify
const, let y el ámbito de bloque
const, let y el ámbito de bloqueProblema: queremos calcular las entradas libres de cada sesión y guardar las funciones de cálculo en un array para ejecutarlas después.
// MAL: var no respeta el ambito de bloque
var acumuladores = [];
var precios = [2500, 2200, 1800];
for (var i = 0; i < precios.length; i++) {
acumuladores.push(function () { return precios[i]; });
}
console.log(acumuladores[0]()); // undefined <- todos comparten el mismo "i", que vale 3// BIEN: let crea una variable nueva en cada vuelta del bucle
const acumuladores = [];
const precios = [2500, 2200, 1800];
for (let i = 0; i < precios.length; i++) {
acumuladores.push(() => precios[i]);
}
console.log(acumuladores[0]()); // 2500| Aspecto | var |
let |
const |
|---|---|---|---|
| Ámbito | Función | Bloque {} |
Bloque {} |
| Reasignable | Sí | Sí | No |
| Redeclarable en el mismo ámbito | Sí | No | No |
| Elevación (hoisting) usable antes de declarar | Sí (vale undefined) |
No (zona muerta temporal) | No |
Se añade a globalThis en el nivel superior |
Sí | No | No |
Regla práctica del curso: usa const por defecto; cambia a let solo cuando de verdad necesites reasignar; no uses var nunca.
Un matiz que confunde: const impide reasignar la variable, no modificar el objeto al que apunta.
const sesion = { id: 'ses-001-1', vendidas: 180 };
sesion.vendidas = 181; // Correcto: modificamos una propiedad
// sesion = {}; // TypeError: Assignment to constant variableSi quieres impedir también la modificación, existe Object.freeze(sesion).
- Funciones flecha y el valor de
this
thisProblema: una clase GestorAforo que suma las entradas vendidas de varias sesiones.
Las funciones flecha son más cortas y, sobre todo, no tienen su propio this: heredan el del contexto donde se escribieron.
// Sintaxis: de la más larga a la más corta
const libres1 = function (sesion) { return sesion.aforo - sesion.vendidas; };
const libres2 = (sesion) => { return sesion.aforo - sesion.vendidas; };
const libres3 = (sesion) => sesion.aforo - sesion.vendidas; // Retorno implícitoCuidado con el retorno implícito de un objeto literal: hay que envolverlo en paréntesis, o JavaScript lee las llaves como un bloque.
const resumen1 = (s) => { id: s.id }; // Devuelve undefined
const resumen2 = (s) => ({ id: s.id }); // CorrectoEl problema clásico de this:
class GestorAforo {
constructor(sesiones) {
this.sesiones = sesiones;
this.total = 0;
}
// MAL: function() crea su propio "this", que aqui vale undefined
totalMal() {
this.sesiones.forEach(function (sesion) {
this.total += sesion.vendidas; // TypeError: Cannot read properties of undefined
});
return this.total;
}
// BIEN: la flecha hereda el "this" del metodo
totalBien() {
this.sesiones.forEach((sesion) => {
this.total += sesion.vendidas;
});
return this.total;
}
}| Usa función flecha cuando... | Usa function cuando... |
|---|---|
Es un callback (map, filter, setTimeout) |
Defines un método de objeto que usa this del objeto |
Quieres heredar el this del entorno |
Necesitas el objeto arguments |
| Es una función corta de una expresión | Necesitas un constructor (new) |
- Desestructuración, parámetros por defecto y propiedades abreviadas
Problema: escribir una función que formatee una sesión sin repetir sesion. en cada línea.
Desestructuración de objetos
const sesion = {
id: 'ses-001-1',
fechaHora: '2026-10-03T20:00:00',
aforo: 420,
vendidas: 180,
precioCentimos: 2500
};
// Extraemos varias propiedades de golpe
const { id, aforo, vendidas } = sesion;
console.log(`${id}: ${aforo - vendidas} libres`); // ses-001-1: 240 libres
// Renombrar al extraer
const { precioCentimos: precio } = sesion;
// Valor por defecto si la propiedad no existe
const { descuentoCentimos = 0 } = sesion;
console.log(descuentoCentimos); // 0Es especialmente útil en los parámetros de una función:
// Sin desestructurar: ruidoso
function describirSesion(sesion) {
return `${sesion.id} | aforo ${sesion.aforo} | vendidas ${sesion.vendidas}`;
}
// Desestructurando en el parámetro: se ve de un vistazo qué necesita la función
function describir({ id, aforo, vendidas }) {
return `${id} | aforo ${aforo} | vendidas ${vendidas}`;
}Desestructuración anidada y de arrays
const evento = {
titulo: 'Noche de Monologos',
sala: 'Sala Boveda',
sesiones: [{ id: 'ses-002-1', aforo: 120 }, { id: 'ses-002-2', aforo: 120 }]
};
// Anidada: la primera sesión y su aforo, en una línea
const { titulo, sesiones: [{ id: primeraSesion }] } = evento;
console.log(titulo, primeraSesion); // Noche de Monologos ses-002-1
// Arrays: por posición
const [primera, segunda] = evento.sesiones;
// Saltarse posiciones con una coma vacía
const partesFecha = '2026-10-03'.split('-');
const [anio, , dia] = partesFecha;
console.log(anio, dia); // 2026 03Parámetros por defecto
// El objeto vacío como valor por defecto evita el error si no se pasa nada
function buscarEventos(catalogo, { sala = null, precioMaximo = Infinity } = {}) {
return catalogo.filter((evento) => {
const coincideSala = sala === null || evento.sala === sala;
const tieneEntradaBarata = evento.sesiones.some(
(s) => s.precioCentimos <= precioMaximo
);
return coincideSala && tieneEntradaBarata;
});
}
buscarEventos(catalogo); // Todos
buscarEventos(catalogo, { sala: 'Sala Boveda' }); // Solo una sala
buscarEventos(catalogo, { precioMaximo: 2000 }); // Solo entradas baratasEsta forma de recibir opciones —un objeto con valores por defecto— es el patrón dominante en el ecosistema Node. Lo verás en Express, en Mongoose y en casi cualquier librería.
Propiedades abreviadas
Cuando la clave y la variable se llaman igual, no hace falta repetirlo:
const sala = 'Teatro Almendra';
const aforo = 420;
const registro = { sala, aforo }; // En vez de { sala: sala, aforo: aforo }
console.log(registro); // { sala: 'Teatro Almendra', aforo: 420 }
// También sirve para métodos
const utilidades = {
formatear(centimos) { // En vez de formatear: function (centimos)
return (centimos / 100).toFixed(2);
}
};
- Propagación y resto: el operador
...
...Los mismos tres puntos hacen dos cosas distintas según dónde estén.
Propagación (spread): desplegar
Problema: crear una copia modificada de una sesión sin alterar la original.
const sesionOriginal = { id: 'ses-002-3', aforo: 120, vendidas: 12, precioCentimos: 1500 };
// Copia con una propiedad cambiada (la original no se toca)
const sesionRebajada = { ...sesionOriginal, precioCentimos: 1200 };
console.log(sesionOriginal.precioCentimos); // 1500
console.log(sesionRebajada.precioCentimos); // 1200El orden importa: lo que va después sobrescribe lo anterior.
const valoresPorDefecto = { aforo: 100, precioCentimos: 2000, vendidas: 0 };
const datosRecibidos = { aforo: 420 };
const sesionFinal = { ...valoresPorDefecto, ...datosRecibidos };
// { aforo: 420, precioCentimos: 2000, vendidas: 0 }Con arrays:
const sesionesOctubre = ['ses-001-1', 'ses-001-2'];
const sesionesAbril = ['ses-003-1', 'ses-003-2'];
const todas = [...sesionesOctubre, ...sesionesAbril];
const copiaSegura = [...todas]; // Para poder ordenar sin estropear "todas"Aviso importante: la propagación hace una copia superficial. Los objetos anidados se comparten.
const copiaEvento = { ...evento }; copiaEvento.sesiones[0].aforo = 999; // ¡También cambia evento.sesiones[0]!Para una copia profunda, Node ofrece
structuredClone(evento).
Resto (rest): recoger
// En parámetros: recoge un número indeterminado de argumentos
function totalCentimos(...precios) {
return precios.reduce((suma, p) => suma + p, 0);
}
totalCentimos(2500, 2200, 1800); // 6500
// En desestructuración: separa lo que te interesa del resto
const { id, ...datosSesion } = sesionOriginal;
console.log(id); // 'ses-002-3'
console.log(datosSesion); // { aforo: 120, vendidas: 12, precioCentimos: 1500 }Este segundo uso es habitual para quitar campos sensibles antes de enviar datos, algo que aplicaremos con los usuarios en el Módulo 8:
const usuario = { id: 'usr-01', email: '[email protected]', contrasenaHash: '$2b$10$...' };
const { contrasenaHash, ...usuarioPublico } = usuario; // usuarioPublico ya no lleva el hash
- Encadenamiento opcional
?. y coalescencia nula ??
?. y coalescencia nula ??Problema: mostrar el nombre del organizador de un evento, sabiendo que algunos eventos antiguos no tienen ese dato.
const eventoAntiguo = { id: 'evt-000', titulo: 'Gala de Reapertura' };
// Sin ?. : error
console.log(eventoAntiguo.organizador.nombre);
// TypeError: Cannot read properties of undefined (reading 'nombre')
// Con ?. : devuelve undefined en lugar de reventar
console.log(eventoAntiguo.organizador?.nombre); // undefined?. corta la evaluación en cuanto encuentra null o undefined. Tiene tres formas:
evento.organizador?.nombre // Propiedad
evento.sesiones?.[0] // Índice de array
evento.calcularAforo?.() // Llamada a método que puede no existir?? (coalescencia nula) devuelve la derecha solo si la izquierda es null o undefined. No confundir con ||:
const sesion = { vendidas: 0, descuento: null };
console.log(sesion.vendidas || 'sin datos'); // 'sin datos' <- MAL: 0 es falsy
console.log(sesion.vendidas ?? 'sin datos'); // 0 <- BIEN| Valor de la izquierda | || devuelve |
?? devuelve |
|---|---|---|
0 |
la derecha | 0 |
'' |
la derecha | '' |
false |
la derecha | false |
null |
la derecha | la derecha |
undefined |
la derecha | la derecha |
En un dominio como el nuestro esto es crítico: 0 entradas vendidas y 0 euros de descuento son valores legítimos. Usar || los borraría.
Combinados resuelven el problema con elegancia:
Existe también el operador de asignación ??=:
const conteo = {};
conteo['Sala Boveda'] ??= 0; // Solo asigna si es null o undefined
conteo['Sala Boveda'] += 1;
- Plantillas de cadena
Problema: generar la línea de una entrada para imprimir.
const entrada = {
codigo: 'EV-2026-000123',
evento: 'Concierto de Otono',
sala: 'Teatro Almendra',
precioCentimos: 2500
};
// Concatenación clásica: ilegible
const linea1 = 'Entrada ' + entrada.codigo + ' - ' + entrada.evento +
' (' + entrada.sala + ') - ' + (entrada.precioCentimos / 100) + ' EUR';
// Plantilla de cadena: se lee de corrido
const linea2 = `Entrada ${entrada.codigo} - ${entrada.evento} (${entrada.sala}) - ${(entrada.precioCentimos / 100).toFixed(2)} EUR`;Ventajas adicionales:
// 1. Saltos de línea reales, sin \n
const justificante = `
ESCENA VIVA
Codigo : ${entrada.codigo}
Evento : ${entrada.evento}
Sala : ${entrada.sala}
Importe: ${(entrada.precioCentimos / 100).toFixed(2)} EUR
`;
// 2. Cualquier expresión dentro de ${}
const sesion = { aforo: 420, vendidas: 180 };
console.log(`Ocupacion: ${Math.round((sesion.vendidas / sesion.aforo) * 100)}%`);
// 3. Condicionales en línea
const agotada = sesion.vendidas >= sesion.aforo;
console.log(`Estado: ${agotada ? 'AGOTADA' : 'DISPONIBLE'}`);
- Métodos de array esenciales aplicados al catálogo
Esta es la sección que más usarás. Todos estos métodos devuelven un array nuevo o un valor sin modificar el original (salvo sort, que sí lo modifica).
| Método | Devuelve | Pregunta que responde |
|---|---|---|
map |
Array del mismo tamaño | "Transforma cada elemento" |
filter |
Array igual o menor | "Quédate con los que cumplan" |
find |
Un elemento o undefined |
"Dame el primero que cumpla" |
findIndex |
Número o -1 |
"¿En qué posición está?" |
reduce |
Cualquier cosa | "Acumula todo en un valor" |
some |
true / false |
"¿Hay alguno que cumpla?" |
every |
true / false |
"¿Cumplen todos?" |
sort |
El mismo array, ordenado | "Ordénalos" (¡modifica!) |
flatMap |
Array aplanado un nivel | "Transforma y aplana" |
includes |
true / false |
"¿Está este valor?" |
Trabajemos con el catálogo de Escena Viva:
// map: lista de títulos para un menú
const titulos = catalogo.map((evento) => evento.titulo);
// [ 'Concierto de Otono', 'Noche de Monologos', 'Festival de Jazz de Primavera' ]
// filter: eventos de una sala
const enBoveda = catalogo.filter((evento) => evento.sala === 'Sala Boveda');
// find: buscar por identificador (devuelve el objeto, no un array)
const evento = catalogo.find((e) => e.id === 'evt-003');
console.log(evento.titulo); // Festival de Jazz de Primavera
// flatMap: todas las sesiones del catálogo en un solo array
const sesiones = catalogo.flatMap((evento) => evento.sesiones);
console.log(sesiones.length); // 7reduce en profundidad
reduce intimida al principio, pero solo tiene dos partes: una función acumuladora y un valor inicial.
// Aforo total de la plataforma
const aforoTotal = sesiones.reduce((acumulado, sesion) => acumulado + sesion.aforo, 0);
console.log(aforoTotal); // 3000Paso a paso: acumulado empieza valiendo 0 (el segundo argumento). En cada vuelta recibe el resultado anterior y la sesión actual, y devuelve el nuevo acumulado.
reduce no sirve solo para sumar; puede construir objetos:
// Entradas vendidas agrupadas por sala
const vendidasPorSala = catalogo.reduce((acumulado, evento) => {
const vendidasEvento = evento.sesiones.reduce((s, ses) => s + ses.vendidas, 0);
acumulado[evento.sala] = (acumulado[evento.sala] ?? 0) + vendidasEvento;
return acumulado; // ¡No olvides devolver el acumulador!
}, {});
console.log(vendidasPorSala);
// { 'Teatro Almendra': 276, 'Sala Boveda': 175, 'Auditorio Ribera': 1360 }Error número uno con
reduce: olvidar elreturn acumuladodentro de la función. El resultado seráundefineden la segunda vuelta.
some y every
// ¿Algún evento tiene entradas por debajo de 20 EUR?
const hayBaratas = catalogo.some((e) => e.sesiones.some((s) => s.precioCentimos < 2000));
console.log(hayBaratas); // true
// ¿Están todas las sesiones por encima del 50% de ocupación?
const todasLlenas = sesiones.every((s) => s.vendidas / s.aforo > 0.5);
console.log(todasLlenas); // falseAmbos cortocircuitan: some para en el primer true, every en el primer false. Sobre catálogos grandes, esto importa.
sort y su trampa
// 1. sort MODIFICA el array. Copia primero.
const porPrecio = [...sesiones].sort((a, b) => a.precioCentimos - b.precioCentimos);
// 2. Sin función de comparación, sort ordena como TEXTO
console.log([10, 9, 100, 20].sort()); // [ 10, 100, 20, 9 ] <- ¡mal!
console.log([10, 9, 100, 20].sort((a, b) => a - b)); // [ 9, 10, 20, 100 ]
// 3. Ordenar por texto con acentos y mayúsculas: localeCompare
const ordenados = [...catalogo].sort((a, b) => a.titulo.localeCompare(b.titulo, 'es'));
// 4. Ordenar por fecha (las cadenas ISO se ordenan bien alfabéticamente)
const porFecha = [...sesiones].sort((a, b) => a.fechaHora.localeCompare(b.fechaHora));Que las fechas en formato ISO AAAA-MM-DDTHH:mm:ss se ordenen correctamente como texto es una de las razones por las que Escena Viva adopta ese formato como convención.
Encadenar métodos
La verdadera potencia está en combinarlos:
// Las tres sesiones más caras con entradas disponibles, formateadas
const destacadas = catalogo
.flatMap((evento) => evento.sesiones.map((s) => ({ ...s, evento: evento.titulo })))
.filter((sesion) => sesion.vendidas < sesion.aforo)
.sort((a, b) => b.precioCentimos - a.precioCentimos)
.slice(0, 3)
.map((s) => `${s.evento}: ${(s.precioCentimos / 100).toFixed(2)} EUR`);
console.log(destacadas);
// [ 'Festival de Jazz de Primavera: 42.00 EUR',
// 'Festival de Jazz de Primavera: 38.00 EUR',
// 'Concierto de Otono: 25.00 EUR' ]Aquí el sort es seguro porque opera sobre el array nuevo que produjo filter.
- Objetos:
Object.keys, values y entries
Object.keys, values y entriesProblema: recorrer el objeto de ventas por sala que acabamos de construir con reduce.
const vendidasPorSala = {
'Teatro Almendra': 276,
'Sala Boveda': 175,
'Auditorio Ribera': 1360
};
Object.keys(vendidasPorSala); // [ 'Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera' ]
Object.values(vendidasPorSala); // [ 276, 175, 1360 ]
Object.entries(vendidasPorSala); // [ [ 'Teatro Almendra', 276 ], ... ]Object.entries combinado con desestructuración es el idioma más limpio para recorrer un objeto:
for (const [sala, vendidas] of Object.entries(vendidasPorSala)) {
console.log(`${sala.padEnd(20)} ${String(vendidas).padStart(5)} entradas`);
}Y en sentido inverso, Object.fromEntries reconstruye un objeto desde pares:
// Ventas por sala expresadas en euros
const enEuros = Object.fromEntries(
Object.entries(vendidasPorSala).map(([sala, n]) => [sala, n * 25])
);Object.assign fusiona objetos (equivale a la propagación, pero modifica el destino):
const base = { aforo: 100, precioCentimos: 2000 };
const combinado = Object.assign({}, base, { aforo: 420 }); // Destino {} vacío: seguro
Map y Set: cuándo usar cada uno
Map y Set: cuándo usar cada unoProblema: obtener la lista de salas sin repetir, e indexar los eventos por su identificador para buscarlos al instante.
Set: colección sin duplicados
const salas = new Set(catalogo.map((evento) => evento.sala));
console.log(salas.size); // 3
console.log(salas.has('Sala Boveda')); // true
console.log([...salas]); // Convertir a array para poder usar map/sort
// Eliminar duplicados de cualquier array en una línea
const categorias = [...new Set(catalogo.map((e) => e.categoria))];Map: diccionario con claves de cualquier tipo
// Índice de eventos por id: buscar pasa a ser instantáneo
const indice = new Map(catalogo.map((evento) => [evento.id, evento]));
console.log(indice.get('evt-002').titulo); // Noche de Monologos
console.log(indice.has('evt-999')); // false
console.log(indice.size); // 3
// Recorrerlo con desestructuración
for (const [id, evento] of indice) {
console.log(`${id} -> ${evento.titulo}`);
}| Aspecto | Objeto {} |
Map |
|---|---|---|
| Tipo de las claves | Solo texto y Symbol |
Cualquiera (objetos, números, fechas) |
| Orden de inserción garantizado | Parcialmente | Sí |
| Tamaño | Object.keys(o).length |
map.size |
| Colisión con propiedades heredadas | Sí (toString, constructor) |
No |
| Serializable a JSON directamente | Sí | No (hay que convertir) |
| Rendimiento en altas y bajas frecuentes | Peor | Mejor |
Regla práctica: usa un objeto cuando las claves son fijas y conocidas (un registro, una configuración, algo que irá a JSON) y un Map cuando las claves son dinámicas, numerosas o no son cadenas (un índice, una caché, un contador).
- Clases, campos y métodos privados
Problema: encapsular la lógica de una sesión —calcular libres, comprobar si está agotada, reservar entradas— para no repetirla por todo el proyecto.
class Evento {
// Campo privado: solo accesible desde dentro de la clase
#sesiones = [];
constructor({ id, titulo, sala, categoria, duracionMinutos, sesiones = [] }) {
this.id = id;
this.titulo = titulo;
this.sala = sala;
this.categoria = categoria;
this.duracionMinutos = duracionMinutos;
this.#sesiones = sesiones;
}
// Getter: se usa como propiedad, sin paréntesis
get numeroSesiones() {
return this.#sesiones.length;
}
get aforoTotal() {
return this.#sesiones.reduce((total, s) => total + s.aforo, 0);
}
get entradasVendidas() {
return this.#sesiones.reduce((total, s) => total + s.vendidas, 0);
}
get ocupacion() {
if (this.aforoTotal === 0) return 0;
return Math.round((this.entradasVendidas / this.aforoTotal) * 100);
}
// Método privado: detalle interno que no forma parte de la API pública
#buscarSesion(idSesion) {
return this.#sesiones.find((s) => s.id === idSesion);
}
entradasLibres(idSesion) {
const sesion = this.#buscarSesion(idSesion);
if (!sesion) return 0;
return sesion.aforo - sesion.vendidas;
}
reservar(idSesion, cantidad = 1) {
const sesion = this.#buscarSesion(idSesion);
if (!sesion) {
throw new Error(`La sesion ${idSesion} no existe en ${this.id}`);
}
if (sesion.aforo - sesion.vendidas < cantidad) {
throw new Error(`Aforo insuficiente en ${idSesion}`);
}
sesion.vendidas += cantidad;
return sesion.vendidas;
}
// toString se invoca automáticamente al interpolar en una plantilla
toString() {
return `${this.titulo} (${this.sala}) - ${this.ocupacion}% ocupado`;
}
// Método estático: pertenece a la clase, no a cada instancia
static desdeJSON(objeto) {
return new Evento(objeto);
}
}Uso:
const concierto = Evento.desdeJSON(catalogo[0]);
console.log(concierto.numeroSesiones); // 2 (getter, sin paréntesis)
console.log(concierto.ocupacion); // 33
console.log(concierto.entradasLibres('ses-001-1')); // 240
console.log(`${concierto}`); // Concierto de Otono (Teatro Almendra) - 33% ocupado
concierto.reservar('ses-001-1', 4);
console.log(concierto.entradasLibres('ses-001-1')); // 236
// Los campos privados son realmente inaccesibles desde fuera
// console.log(concierto.#sesiones); // SyntaxError en tiempo de compilaciónSobre los campos privados #:
- Son privacidad real, aplicada por el motor, no una convención como el prefijo
_. - Solo se puede acceder a ellos dentro del cuerpo de la clase.
- No aparecen en
Object.keys(), ni enJSON.stringify(), ni al inspeccionar el objeto. - Se pueden comprobar con
#campo in objetopara saber si una instancia es de esa clase.
- JSON:
parse y stringify
parse y stringifyJSON es el formato de intercambio de todo el curso: el fichero semilla datos/eventos.json, las respuestas de la API, la configuración. JavaScript lo maneja de forma nativa.
// De objeto a texto
const texto = JSON.stringify(catalogo[0]);
// {"id":"evt-001","titulo":"Concierto de Otono",...} todo en una línea
// Con formato legible: el tercer argumento es la indentación
const textoLegible = JSON.stringify(catalogo[0], null, 2);
console.log(textoLegible);{
"id": "evt-001",
"titulo": "Concierto de Otono",
"sala": "Teatro Almendra",
"sesiones": [
{
"id": "ses-001-1",
"aforo": 420
}
]
}Usa siempre null, 2 para ficheros que vaya a leer una persona (como datos/eventos.json): el control de versiones mostrará diferencias línea a línea en lugar de una única línea gigante modificada.
El segundo argumento (null arriba) es un filtro, muy útil para seleccionar campos:
// Solo estos campos en la salida
JSON.stringify(catalogo[0], ['id', 'titulo', 'sala'], 2);
// O una función, para transformar o excluir valores
JSON.stringify(usuario, (clave, valor) => (clave === 'contrasenaHash' ? undefined : valor), 2);De texto a objeto:
JSON.parse lanza una excepción si el texto no es JSON válido. Envuélvelo siempre en try/catch cuando el origen no sea de confianza:
function leerCatalogoSeguro(texto) {
try {
return JSON.parse(texto);
} catch (error) {
console.error(`JSON invalido: ${error.message}`);
return [];
}
}Limitaciones que debes conocer:
| Tipo en JavaScript | Al pasar por JSON.stringify |
|---|---|
Date |
Se convierte en cadena ISO; al volver es texto, no Date |
undefined |
Desaparece de los objetos; se convierte en null en los arrays |
| Función | Desaparece |
Map, Set |
Se convierten en {} — hay que transformarlos a mano |
NaN, Infinity |
Se convierten en null |
BigInt |
Lanza TypeError |
| Referencia circular | Lanza TypeError |
Que las fechas se conviertan en texto es otra razón para nuestra convención: guardamos las fechas como cadenas ISO desde el principio, así el viaje de ida y vuelta por JSON no pierde nada.
Errores Comunes y Consejos
Error 1: usar || para valores por defecto numéricos.
sesion.vendidas || 100 devuelve 100 cuando hay 0 vendidas. Usa ??.
Error 2: creer que const congela el objeto.
const protege la variable, no su contenido. Usa Object.freeze si necesitas inmutabilidad.
Error 3: olvidar el return en reduce.
Es el fallo más frecuente. Si el resultado es undefined, revisa esto primero.
Error 4: llamar a sort sin copiar.
Modifica el array original y provoca fallos difíciles de rastrear. [...array].sort(...).
Error 5: sort() sin comparador sobre números.
Ordena como texto: [10, 9] queda [10, 9]. Pasa siempre (a, b) => a - b.
Error 6: retorno implícito de un objeto en una flecha.
(s) => { id: s.id } devuelve undefined. Envuelve en paréntesis: (s) => ({ id: s.id }).
Error 7: creer que la propagación copia en profundidad.
{ ...evento } comparte el array sesiones con el original. Usa structuredClone.
Consejo 1: encadena, pero con medida. Tres o cuatro métodos encadenados se leen muy bien; diez, no. Extrae pasos a variables con nombre.
Consejo 2: desestructura en los parámetros. Deja claro qué necesita cada función y documenta la firma sin escribir un solo comentario.
Consejo 3: prototipa en el REPL. Todas las transformaciones de esta lección se pueden probar en segundos con .load src/catalogo-datos.js.
Consejo 4: prefiere métodos que no modifiquen. map, filter y la propagación devuelven cosas nuevas; el código resultante es mucho más fácil de razonar.
Ejercicios
Ejercicio 1: informe de salas
Escribe una función informeSalas(catalogo) que devuelva un array de objetos con esta forma, ordenado de mayor a menor ocupación:
[
{ sala: 'Auditorio Ribera', eventos: 1, sesiones: 2, aforo: 1800, vendidas: 1360, ocupacion: 76 },
...
]Requisitos: usa reduce para agrupar, Object.entries para recorrer el resultado y sort sobre una copia. Muestra el resultado con console.table.
Ejercicio 2: la clase Sesion
Crea una clase Sesion con:
- Constructor que reciba un objeto desestructurado (
id,fechaHora,aforo,vendidas,precioCentimos). - Un campo privado
#vendidas. - Getters
libres,ocupacion(entero de 0 a 100),agotada(booleano) yprecioEuros(cadena con dos decimales). - Método
vender(cantidad = 1)que lance unErrorsi no hay aforo suficiente y devuelva el número de entradas vendidas tras la operación. - Método
toJSON()que devuelva un objeto plano listo paraJSON.stringify(incluyendovendidas, que es privado).
Pruébala con ses-002-1 (120 de aforo, 118 vendidas): vende 2 entradas y comprueba que queda agotada; después intenta vender una más y captura el error.
Ejercicio 3: catálogo público
Escribe catalogoPublico(catalogo, opciones) que prepare los datos para enviarlos a un cliente web. Debe:
- Aceptar
opciones = { sala, precioMaximoCentimos, soloDisponibles = true }, todas opcionales, con desestructuración y valores por defecto. - Filtrar por sala si se indica.
- Descartar las sesiones agotadas si
soloDisponiblesestrue. - Descartar las sesiones que superen
precioMaximoCentimossi se indica. - Eliminar del resultado los eventos que se hayan quedado sin ninguna sesión.
- Devolver los eventos sin el campo
categoriay con las sesiones transformadas a{ id, fechaHora, libres, precioEuros }. - Imprimir el resultado con
JSON.stringify(resultado, null, 2).
Soluciones
Solución 1
function informeSalas(catalogo) {
// 1. Agrupamos por sala con reduce.
const agrupado = catalogo.reduce((acumulado, evento) => {
// ??= crea la entrada la primera vez que aparece la sala.
acumulado[evento.sala] ??= { eventos: 0, sesiones: 0, aforo: 0, vendidas: 0 };
const registro = acumulado[evento.sala];
registro.eventos += 1;
registro.sesiones += evento.sesiones.length;
for (const sesion of evento.sesiones) {
registro.aforo += sesion.aforo;
registro.vendidas += sesion.vendidas;
}
return acumulado; // Imprescindible
}, {});
// 2. Pasamos de objeto a array anadiendo el nombre de la sala y la ocupacion.
const filas = Object.entries(agrupado).map(([sala, datos]) => ({
sala,
...datos,
ocupacion: Math.round((datos.vendidas / datos.aforo) * 100)
}));
// 3. Ordenamos sobre una copia (filas ya es un array nuevo, pero mantenemos el habito).
return [...filas].sort((a, b) => b.ocupacion - a.ocupacion);
}
console.table(informeSalas(catalogo));┌─────────┬────────────────────┬─────────┬──────────┬───────┬──────────┬───────────┐ │ (index) │ sala │ eventos │ sesiones │ aforo │ vendidas │ ocupacion │ ├─────────┼────────────────────┼─────────┼──────────┼───────┼──────────┼───────────┤ │ 0 │ 'Auditorio Ribera' │ 1 │ 2 │ 1800 │ 1360 │ 76 │ │ 1 │ 'Sala Boveda' │ 1 │ 3 │ 360 │ 175 │ 49 │ │ 2 │ 'Teatro Almendra' │ 1 │ 2 │ 840 │ 276 │ 33 │ └─────────┴────────────────────┴─────────┴──────────┴───────┴──────────┴───────────┘
Solución 2
class Sesion {
#vendidas = 0;
constructor({ id, fechaHora, aforo, vendidas = 0, precioCentimos }) {
this.id = id;
this.fechaHora = fechaHora;
this.aforo = aforo;
this.precioCentimos = precioCentimos;
this.#vendidas = vendidas;
}
get vendidas() {
return this.#vendidas;
}
get libres() {
return this.aforo - this.#vendidas;
}
get ocupacion() {
return Math.round((this.#vendidas / this.aforo) * 100);
}
get agotada() {
return this.libres === 0;
}
get precioEuros() {
return (this.precioCentimos / 100).toFixed(2);
}
vender(cantidad = 1) {
if (!Number.isInteger(cantidad) || cantidad < 1) {
throw new Error('La cantidad debe ser un entero positivo');
}
if (cantidad > this.libres) {
throw new Error(`Aforo insuficiente en ${this.id}: quedan ${this.libres}`);
}
this.#vendidas += cantidad;
return this.#vendidas;
}
// JSON.stringify llama automaticamente a toJSON si existe.
toJSON() {
return {
id: this.id,
fechaHora: this.fechaHora,
aforo: this.aforo,
vendidas: this.#vendidas,
precioCentimos: this.precioCentimos
};
}
}
// Prueba
const sesion = new Sesion({
id: 'ses-002-1',
fechaHora: '2026-10-10T21:30:00',
aforo: 120,
vendidas: 118,
precioCentimos: 1800
});
console.log(sesion.libres); // 2
sesion.vender(2);
console.log(sesion.agotada); // true
console.log(sesion.ocupacion); // 100
console.log(sesion.precioEuros); // 18.00
try {
sesion.vender();
} catch (error) {
console.error(`No se pudo vender: ${error.message}`);
// No se pudo vender: Aforo insuficiente en ses-002-1: quedan 0
}
console.log(JSON.stringify(sesion, null, 2)); // Incluye "vendidas": 120Sin toJSON(), el campo privado #vendidas no aparecería en la serialización: es exactamente el caso de uso para el que existe ese método.
Solución 3
function catalogoPublico(
catalogo,
{ sala = null, precioMaximoCentimos = Infinity, soloDisponibles = true } = {}
) {
return catalogo
// 2. Filtro por sala (si no se indica, pasan todos).
.filter((evento) => sala === null || evento.sala === sala)
// 3, 4 y 6. Transformamos cada evento y sus sesiones.
.map((evento) => {
// Quitamos "categoria" con desestructuracion + resto.
const { categoria, sesiones, ...datosPublicos } = evento;
const sesionesFiltradas = sesiones
.filter((s) => !soloDisponibles || s.vendidas < s.aforo)
.filter((s) => s.precioCentimos <= precioMaximoCentimos)
.map((s) => ({
id: s.id,
fechaHora: s.fechaHora,
libres: s.aforo - s.vendidas,
precioEuros: (s.precioCentimos / 100).toFixed(2)
}));
return { ...datosPublicos, sesiones: sesionesFiltradas };
})
// 5. Fuera los eventos que se hayan quedado sin sesiones.
.filter((evento) => evento.sesiones.length > 0);
}
// Prueba: entradas de hasta 20 EUR en la Sala Boveda
const resultado = catalogoPublico(catalogo, {
sala: 'Sala Boveda',
precioMaximoCentimos: 2000
});
console.log(JSON.stringify(resultado, null, 2));[
{
"id": "evt-002",
"titulo": "Noche de Monologos",
"sala": "Sala Boveda",
"duracionMinutos": 80,
"sesiones": [
{ "id": "ses-002-1", "fechaHora": "2026-10-10T21:30:00", "libres": 2, "precioEuros": "18.00" },
{ "id": "ses-002-2", "fechaHora": "2026-10-11T21:30:00", "libres": 75, "precioEuros": "18.00" },
{ "id": "ses-002-3", "fechaHora": "2026-10-17T21:30:00", "libres": 108, "precioEuros": "15.00" }
]
}
]Fíjate en cómo const { categoria, sesiones, ...datosPublicos } = evento cumple dos funciones a la vez: extrae lo que necesitamos tratar aparte y elimina categoria del resultado sin escribir un solo delete.
Conclusión
Este repaso ha recorrido el JavaScript moderno que usarás en cada línea del resto del curso, siempre resolviendo problemas reales del catálogo: const/let y el ámbito de bloque, funciones flecha y el comportamiento de this, desestructuración y parámetros por defecto como forma de recibir opciones, el doble uso del operador ... para copiar y para recoger, ?. y ?? para tratar datos incompletos sin reventar y sin borrar los ceros legítimos, plantillas de cadena, el arsenal de métodos de array —con especial atención a reduce y a las trampas de sort—, Object.entries para recorrer registros, Map y Set para índices y colecciones únicas, clases con getters y campos privados #, y JSON con su null, 2 obligatorio y sus limitaciones de tipos.
Han quedado fijadas por el camino varias decisiones del proyecto que ya no cambiarán: precios en céntimos enteros, fechas como cadenas ISO (que además se ordenan bien como texto) y objetos de opciones con valores por defecto como firma habitual de nuestras funciones.
Falta la otra mitad del JavaScript de servidor: la asincronía. Promesas, async/await y el orden real en que se ejecutan las cosas son el eje del Módulo 2, y ahí es donde el modelo de la primera lección —el camarero que no espera— se convierte en código.
Antes de eso, cerramos el módulo con la lección que da sentido a todo: El Proyecto del Curso: la Plataforma Escena Viva. Formalizaremos el modelo de dominio, escribiremos el fichero semilla datos/eventos.json que sustituirá a los datos incrustados en el código, fijaremos las convenciones del proyecto y verás la hoja de ruta completa: qué construirás en cada uno de los doce módulos y cómo encaja cada pieza en la plataforma final.
Curso de Node.js: De Principiante a Avanzado
Módulo 1: Introducción a Node.js
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
