A les lliçons anteriors han anat apareixent, sense explicació formal, eines com flatMap, l'operador ..., la coalescència nul·la ?? o les funcions fletxa. És hora de posar-los nom. Node.js incorpora un motor V8 molt actualitzat, cosa que significa que pots fer servir tot el JavaScript modern sense transpiladors ni configuració: no hi ha navegadors antics als quals donar suport al servidor.

Aquesta lliçó no és una referència del llenguatge —per a això hi ha la documentació—, sinó un repàs pràctic: cada apartat resol un problema real del catàleg d'Escena Viva. Si ja domines aquests conceptes, llegeix-la com una llista de comprovació; si véns d'un JavaScript més antic, aquí hi ha la base de tot el codi del curs.

Un avís previ: async/await i les promeses no es tracten aquí. Són el cor del Mòdul 2 i mereixen el seu propi espai.

Contingut

  1. const, let i l'àmbit de bloc
  2. Funcions fletxa i el valor de this
  3. Desestructuració, paràmetres per defecte i propietats abreujades
  4. Propagació i resta: l'operador ...
  5. Encadenament opcional ?. i coalescència nul·la ??
  6. Plantilles de cadena
  7. Mètodes d'array essencials aplicats al catàleg
  8. Objectes: Object.keys, values i entries
  9. Map i Set: quan fer servir cadascun
  10. Classes, camps i mètodes privats
  11. JSON: parse i stringify

  1. const, let i l'àmbit de bloc

Problema: volem calcular les entrades lliures de cada sessió i desar les funcions de càlcul en un array per executar-les després.

// MALAMENT: var no respecta l'ambit de bloc
var acumuladors = [];
var preus = [2500, 2200, 1800];

for (var i = 0; i < preus.length; i++) {
  acumuladors.push(function () { return preus[i]; });
}

console.log(acumuladors[0]());   // undefined  <- tots comparteixen la mateixa "i", que val 3
// BE: let crea una variable nova a cada volta del bucle
const acumuladors = [];
const preus = [2500, 2200, 1800];

for (let i = 0; i < preus.length; i++) {
  acumuladors.push(() => preus[i]);
}

console.log(acumuladors[0]());   // 2500
Aspecte var let const
Àmbit Funció Bloc {} Bloc {}
Reassignable Sí Sí No
Redeclarable al mateix àmbit Sí No No
Elevació (hoisting) utilitzable abans de declarar Sí (val undefined) No (zona morta temporal) No
S'afegeix a globalThis al nivell superior Sí No No

Regla pràctica del curs: fes servir const per defecte; canvia a let només quan de debò necessitis reassignar; no facis servir mai var.

Un matís que confon: const impedeix reassignar la variable, no modificar l'objecte al qual apunta.

const sessio = { id: 'ses-001-1', venudes: 180 };

sessio.venudes = 181;   // Correcte: modifiquem una propietat
// sessio = {};         // TypeError: Assignment to constant variable

Si vols impedir també la modificació, existeix Object.freeze(sessio).

  1. Funcions fletxa i el valor de this

Problema: una classe GestorAforament que suma les entrades venudes de diverses sessions.

Les funcions fletxa són més curtes i, sobretot, no tenen el seu propi this: hereten el del context on es van escriure.

// Sintaxi: de la mes llarga a la mes curta
const lliures1 = function (sessio) { return sessio.aforament - sessio.venudes; };
const lliures2 = (sessio) => { return sessio.aforament - sessio.venudes; };
const lliures3 = (sessio) => sessio.aforament - sessio.venudes;   // Retorn implicit

Compte amb el retorn implícit d'un objecte literal: cal embolcallar-lo entre parèntesis, o JavaScript llegeix les claus com un bloc.

const resum1 = (s) => { id: s.id };        // Retorna undefined
const resum2 = (s) => ({ id: s.id });      // Correcte

El problema clàssic de this:

class GestorAforament {
  constructor(sessions) {
    this.sessions = sessions;
    this.total = 0;
  }

  // MALAMENT: function() crea el seu propi "this", que aqui val undefined
  totalMalament() {
    this.sessions.forEach(function (sessio) {
      this.total += sessio.venudes;   // TypeError: Cannot read properties of undefined
    });
    return this.total;
  }

  // BE: la fletxa hereta el "this" del metode
  totalBe() {
    this.sessions.forEach((sessio) => {
      this.total += sessio.venudes;
    });
    return this.total;
  }
}
Fes servir funció fletxa quan... Fes servir function quan...
És un callback (map, filter, setTimeout) Defineixes un mètode d'objecte que fa servir el this de l'objecte
Vols heretar el this de l'entorn Necessites l'objecte arguments
És una funció curta d'una expressió Necessites un constructor (new)

  1. Desestructuració, paràmetres per defecte i propietats abreujades

Problema: escriure una funció que formati una sessió sense repetir sessio. a cada línia.

Desestructuració d'objectes

const sessio = {
  id: 'ses-001-1',
  dataHora: '2026-10-03T20:00:00',
  aforament: 420,
  venudes: 180,
  preuCentims: 2500
};

// Extraiem diverses propietats de cop
const { id, aforament, venudes } = sessio;
console.log(`${id}: ${aforament - venudes} lliures`);   // ses-001-1: 240 lliures

// Reanomenar en extreure
const { preuCentims: preu } = sessio;

// Valor per defecte si la propietat no existeix
const { descompteCentims = 0 } = sessio;
console.log(descompteCentims);   // 0

És especialment útil als paràmetres d'una funció:

// Sense desestructurar: sorollos
function descriureSessio(sessio) {
  return `${sessio.id} | aforament ${sessio.aforament} | venudes ${sessio.venudes}`;
}

// Desestructurant al parametre: es veu d'un cop d'ull que necessita la funcio
function descriure({ id, aforament, venudes }) {
  return `${id} | aforament ${aforament} | venudes ${venudes}`;
}

Desestructuració imbricada i d'arrays

const esdeveniment = {
  titol: 'Noche de Monologos',
  sala: 'Sala Boveda',
  sessions: [{ id: 'ses-002-1', aforament: 120 }, { id: 'ses-002-2', aforament: 120 }]
};

// Imbricada: la primera sessio i el seu identificador, en una linia
const { titol, sessions: [{ id: primeraSessio }] } = esdeveniment;
console.log(titol, primeraSessio);   // Noche de Monologos ses-002-1

// Arrays: per posicio
const [primera, segona] = esdeveniment.sessions;

// Saltar-se posicions amb una coma buida
const partsData = '2026-10-03'.split('-');
const [any, , dia] = partsData;
console.log(any, dia);   // 2026 03

Paràmetres per defecte

// L'objecte buit com a valor per defecte evita l'error si no es passa res
function cercarEsdeveniments(cataleg, { sala = null, preuMaxim = Infinity } = {}) {
  return cataleg.filter((esdeveniment) => {
    const coincideixSala = sala === null || esdeveniment.sala === sala;
    const teEntradaBarata = esdeveniment.sessions.some(
      (s) => s.preuCentims <= preuMaxim
    );
    return coincideixSala && teEntradaBarata;
  });
}

cercarEsdeveniments(cataleg);                              // Tots
cercarEsdeveniments(cataleg, { sala: 'Sala Boveda' });     // Nomes una sala
cercarEsdeveniments(cataleg, { preuMaxim: 2000 });         // Nomes entrades barates

Aquesta manera de rebre opcions —un objecte amb valors per defecte— és el patró dominant a l'ecosistema Node. El veuràs a Express, a Mongoose i a gairebé qualsevol biblioteca.

Propietats abreujades

Quan la clau i la variable es diuen igual, no cal repetir-ho:

const sala = 'Teatro Almendra';
const aforament = 420;

const registre = { sala, aforament };        // En comptes de { sala: sala, aforament: aforament }
console.log(registre);                        // { sala: 'Teatro Almendra', aforament: 420 }

// Tambe serveix per a metodes
const utils = {
  formatar(centims) {                         // En comptes de formatar: function (centims)
    return (centims / 100).toFixed(2);
  }
};

  1. Propagació i resta: l'operador ...

Els mateixos tres punts fan dues coses diferents segons on siguin.

Propagació (spread): desplegar

Problema: crear una còpia modificada d'una sessió sense alterar l'original.

const sessioOriginal = { id: 'ses-002-3', aforament: 120, venudes: 12, preuCentims: 1500 };

// Copia amb una propietat canviada (l'original no es toca)
const sessioRebaixada = { ...sessioOriginal, preuCentims: 1200 };

console.log(sessioOriginal.preuCentims);   // 1500
console.log(sessioRebaixada.preuCentims);  // 1200

L'ordre importa: el que va després sobreescriu l'anterior.

const valorsPerDefecte = { aforament: 100, preuCentims: 2000, venudes: 0 };
const dadesRebudes = { aforament: 420 };

const sessioFinal = { ...valorsPerDefecte, ...dadesRebudes };
// { aforament: 420, preuCentims: 2000, venudes: 0 }

Amb arrays:

const sessionsOctubre = ['ses-001-1', 'ses-001-2'];
const sessionsAbril = ['ses-003-1', 'ses-003-2'];

const totes = [...sessionsOctubre, ...sessionsAbril];
const copiaSegura = [...totes];               // Per poder ordenar sense espatllar "totes"

Avís important: la propagació fa una còpia superficial. Els objectes imbricats es comparteixen.

const copiaEsdeveniment = { ...esdeveniment };
copiaEsdeveniment.sessions[0].aforament = 999;   // També canvia esdeveniment.sessions[0]!

Per a una còpia profunda, Node ofereix structuredClone(esdeveniment).

Resta (rest): recollir

// En parametres: recull un nombre indeterminat d'arguments
function totalCentims(...preus) {
  return preus.reduce((suma, p) => suma + p, 0);
}

totalCentims(2500, 2200, 1800);   // 6500

// En desestructuracio: separa el que t'interessa de la resta
const { id, ...dadesSessio } = sessioOriginal;
console.log(id);            // 'ses-002-3'
console.log(dadesSessio);   // { aforament: 120, venudes: 12, preuCentims: 1500 }

Aquest segon ús és habitual per treure camps sensibles abans d'enviar dades, cosa que aplicarem amb els usuaris al Mòdul 8:

const usuari = { id: 'usr-01', email: '[email protected]', hashContrasenya: '$2b$10$...' };
const { hashContrasenya, ...usuariPublic } = usuari;   // usuariPublic ja no porta el hash

  1. Encadenament opcional ?. i coalescència nul·la ??

Problema: mostrar el nom de l'organitzador d'un esdeveniment, sabent que alguns esdeveniments antics no tenen aquesta dada.

const esdevenimentAntic = { id: 'evt-000', titol: 'Gala de Reapertura' };

// Sense ?. : error
console.log(esdevenimentAntic.organitzador.nom);
// TypeError: Cannot read properties of undefined (reading 'nom')

// Amb ?. : retorna undefined en comptes de rebentar
console.log(esdevenimentAntic.organitzador?.nom);   // undefined

?. talla l'avaluació tan bon punt troba null o undefined. Té tres formes:

esdeveniment.organitzador?.nom        // Propietat
esdeveniment.sessions?.[0]            // Index d'array
esdeveniment.calcularAforament?.()    // Crida a metode que pot no existir

?? (coalescència nul·la) retorna la dreta només si l'esquerra és null o undefined. No s'ha de confondre amb ||:

const sessio = { venudes: 0, descompte: null };

console.log(sessio.venudes || 'sense dades');   // 'sense dades'  <- MALAMENT: 0 es falsy
console.log(sessio.venudes ?? 'sense dades');   // 0              <- BE
Valor de l'esquerra || retorna ?? retorna
0 la dreta 0
'' la dreta ''
false la dreta false
null la dreta la dreta
undefined la dreta la dreta

En un domini com el nostre això és crític: 0 entrades venudes i 0 euros de descompte són valors legítims. Fer servir || els esborraria.

Combinats resolen el problema amb elegància:

function nomOrganitzador(esdeveniment) {
  return esdeveniment.organitzador?.nom ?? 'Escena Viva';
}

Existeix també l'operador d'assignació ??=:

const recompte = {};
recompte['Sala Boveda'] ??= 0;    // Nomes assigna si es null o undefined
recompte['Sala Boveda'] += 1;

  1. Plantilles de cadena

Problema: generar la línia d'una entrada per imprimir.

const entrada = {
  codi: 'EV-2026-000123',
  esdeveniment: 'Concierto de Otono',
  sala: 'Teatro Almendra',
  preuCentims: 2500
};

// Concatenacio classica: illegible
const linia1 = 'Entrada ' + entrada.codi + ' - ' + entrada.esdeveniment +
               ' (' + entrada.sala + ') - ' + (entrada.preuCentims / 100) + ' EUR';

// Plantilla de cadena: es llegeix de corregut
const linia2 = `Entrada ${entrada.codi} - ${entrada.esdeveniment} (${entrada.sala}) - ${(entrada.preuCentims / 100).toFixed(2)} EUR`;

Avantatges addicionals:

// 1. Salts de linia reals, sense \n
const justificant = `
ESCENA VIVA
Codi    : ${entrada.codi}
Acte    : ${entrada.esdeveniment}
Sala    : ${entrada.sala}
Import  : ${(entrada.preuCentims / 100).toFixed(2)} EUR
`;

// 2. Qualsevol expressio dins de ${}
const sessio = { aforament: 420, venudes: 180 };
console.log(`Ocupacio: ${Math.round((sessio.venudes / sessio.aforament) * 100)}%`);

// 3. Condicionals en linia
const exhaurida = sessio.venudes >= sessio.aforament;
console.log(`Estat: ${exhaurida ? 'EXHAURIDA' : 'DISPONIBLE'}`);

  1. Mètodes d'array essencials aplicats al catàleg

Aquesta és la secció que més faràs servir. Tots aquests mètodes retornen un array nou o un valor sense modificar l'original (llevat de sort, que sí que el modifica).

Mètode Retorna Pregunta que respon
map Array de la mateixa mida «Transforma cada element»
filter Array igual o menor «Queda't amb els que compleixin»
find Un element o undefined «Dona'm el primer que compleixi»
findIndex Nombre o -1 «En quina posició és?»
reduce Qualsevol cosa «Acumula-ho tot en un valor»
some true / false «N'hi ha algun que compleixi?»
every true / false «Compleixen tots?»
sort El mateix array, ordenat «Ordena'ls» (modifica!)
flatMap Array aplanat un nivell «Transforma i aplana»
includes true / false «Hi ha aquest valor?»

Treballem amb el catàleg d'Escena Viva:

// map: llista de titols per a un menu
const titols = cataleg.map((esdeveniment) => esdeveniment.titol);
// [ 'Concierto de Otono', 'Noche de Monologos', 'Festival de Jazz de Primavera' ]

// filter: esdeveniments d'una sala
const aBoveda = cataleg.filter((esdeveniment) => esdeveniment.sala === 'Sala Boveda');

// find: cercar per identificador (retorna l'objecte, no un array)
const esdeveniment = cataleg.find((e) => e.id === 'evt-003');
console.log(esdeveniment.titol);   // Festival de Jazz de Primavera

// flatMap: totes les sessions del cataleg en un sol array
const sessions = cataleg.flatMap((esdeveniment) => esdeveniment.sessions);
console.log(sessions.length);   // 7

reduce a fons

reduce intimida al principi, però només té dues parts: una funció acumuladora i un valor inicial.

// Aforament total de la plataforma
const aforamentTotal = sessions.reduce((acumulat, sessio) => acumulat + sessio.aforament, 0);
console.log(aforamentTotal);   // 3000

Pas a pas: acumulat comença valent 0 (el segon argument). A cada volta rep el resultat anterior i la sessió actual, i retorna el nou acumulat.

reduce no serveix només per sumar; pot construir objectes:

// Entrades venudes agrupades per sala
const venudesPerSala = cataleg.reduce((acumulat, esdeveniment) => {
  const venudesEsdeveniment = esdeveniment.sessions.reduce((s, ses) => s + ses.venudes, 0);
  acumulat[esdeveniment.sala] = (acumulat[esdeveniment.sala] ?? 0) + venudesEsdeveniment;
  return acumulat;   // No oblidis retornar l'acumulador!
}, {});

console.log(venudesPerSala);
// { 'Teatro Almendra': 276, 'Sala Boveda': 175, 'Auditorio Ribera': 1360 }

Error número u amb reduce: oblidar el return acumulat dins de la funció. El resultat serà undefined a la segona volta.

some i every

// Hi ha algun esdeveniment amb entrades per sota de 20 EUR?
const hiHaBarates = cataleg.some((e) => e.sessions.some((s) => s.preuCentims < 2000));
console.log(hiHaBarates);   // true

// Son totes les sessions per sobre del 50% d'ocupacio?
const totesPlenes = sessions.every((s) => s.venudes / s.aforament > 0.5);
console.log(totesPlenes);   // false

Tots dos fan curtcircuit: some s'atura al primer true, every al primer false. Sobre catàlegs grans, això importa.

sort i el seu parany

// 1. sort MODIFICA l'array. Copia'l primer.
const perPreu = [...sessions].sort((a, b) => a.preuCentims - b.preuCentims);

// 2. Sense funcio de comparacio, sort ordena com a TEXT
console.log([10, 9, 100, 20].sort());              // [ 10, 100, 20, 9 ]  <- malament!
console.log([10, 9, 100, 20].sort((a, b) => a - b)); // [ 9, 10, 20, 100 ]

// 3. Ordenar per text amb accents i majuscules: localeCompare
const ordenats = [...cataleg].sort((a, b) => a.titol.localeCompare(b.titol, 'ca'));

// 4. Ordenar per data (les cadenes ISO s'ordenen be alfabeticament)
const perData = [...sessions].sort((a, b) => a.dataHora.localeCompare(b.dataHora));

Que les dates en format ISO AAAA-MM-DDTHH:mm:ss s'ordenin correctament com a text és una de les raons per les quals Escena Viva adopta aquest format com a convenció.

Encadenar mètodes

La veritable potència és a combinar-los:

// Les tres sessions mes cares amb entrades disponibles, formatades
const destacades = cataleg
  .flatMap((esdeveniment) => esdeveniment.sessions.map((s) => ({ ...s, esdeveniment: esdeveniment.titol })))
  .filter((sessio) => sessio.venudes < sessio.aforament)
  .sort((a, b) => b.preuCentims - a.preuCentims)
  .slice(0, 3)
  .map((s) => `${s.esdeveniment}: ${(s.preuCentims / 100).toFixed(2)} EUR`);

console.log(destacades);
// [ '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 és segur perquè opera sobre l'array nou que ha produït filter.

  1. Objectes: Object.keys, values i entries

Problema: recórrer l'objecte de vendes per sala que acabem de construir amb reduce.

const venudesPerSala = {
  'Teatro Almendra': 276,
  'Sala Boveda': 175,
  'Auditorio Ribera': 1360
};

Object.keys(venudesPerSala);     // [ 'Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera' ]
Object.values(venudesPerSala);   // [ 276, 175, 1360 ]
Object.entries(venudesPerSala);  // [ [ 'Teatro Almendra', 276 ], ... ]

Object.entries combinat amb desestructuració és l'idioma més net per recórrer un objecte:

for (const [sala, venudes] of Object.entries(venudesPerSala)) {
  console.log(`${sala.padEnd(20)} ${String(venudes).padStart(5)} entrades`);
}
Teatro Almendra        276 entrades
Sala Boveda            175 entrades
Auditorio Ribera      1360 entrades

I en sentit invers, Object.fromEntries reconstrueix un objecte a partir de parells:

// Vendes per sala expressades en euros
const enEuros = Object.fromEntries(
  Object.entries(venudesPerSala).map(([sala, n]) => [sala, n * 25])
);

Object.assign fusiona objectes (equival a la propagació, però modifica el destí):

const base = { aforament: 100, preuCentims: 2000 };
const combinat = Object.assign({}, base, { aforament: 420 });   // Desti {} buit: segur

  1. Map i Set: quan fer servir cadascun

Problema: obtenir la llista de sales sense repetir, i indexar els esdeveniments pel seu identificador per trobar-los a l'instant.

Set: col·lecció sense duplicats

const sales = new Set(cataleg.map((esdeveniment) => esdeveniment.sala));

console.log(sales.size);                    // 3
console.log(sales.has('Sala Boveda'));      // true
console.log([...sales]);                    // Convertir a array per poder fer servir map/sort

// Eliminar duplicats de qualsevol array en una linia
const categories = [...new Set(cataleg.map((e) => e.categoria))];

Map: diccionari amb claus de qualsevol tipus

// Index d'esdeveniments per id: cercar passa a ser instantani
const index = new Map(cataleg.map((esdeveniment) => [esdeveniment.id, esdeveniment]));

console.log(index.get('evt-002').titol);   // Noche de Monologos
console.log(index.has('evt-999'));         // false
console.log(index.size);                   // 3

// Recorrer-lo amb desestructuracio
for (const [id, esdeveniment] of index) {
  console.log(`${id} -> ${esdeveniment.titol}`);
}
Aspecte Objecte {} Map
Tipus de les claus Només text i Symbol Qualsevol (objectes, nombres, dates)
Ordre d'inserció garantit Parcialment Sí
Mida Object.keys(o).length map.size
Col·lisió amb propietats heretades Sí (toString, constructor) No
Serialitzable a JSON directament Sí No (cal convertir-lo)
Rendiment en altes i baixes freqüents Pitjor Millor

Regla pràctica: fes servir un objecte quan les claus són fixes i conegudes (un registre, una configuració, una cosa que anirà a JSON) i un Map quan les claus són dinàmiques, nombroses o no són cadenes (un índex, una memòria cau, un comptador).

  1. Classes, camps i mètodes privats

Problema: encapsular la lògica d'una sessió —calcular lliures, comprovar si està exhaurida, reservar entrades— per no repetir-la per tot el projecte.

class Esdeveniment {
  // Camp privat: nomes accessible des de dins de la classe
  #sessions = [];

  constructor({ id, titol, sala, categoria, duracioMinuts, sessions = [] }) {
    this.id = id;
    this.titol = titol;
    this.sala = sala;
    this.categoria = categoria;
    this.duracioMinuts = duracioMinuts;
    this.#sessions = sessions;
  }

  // Getter: es fa servir com a propietat, sense parentesis
  get nombreSessions() {
    return this.#sessions.length;
  }

  get aforamentTotal() {
    return this.#sessions.reduce((total, s) => total + s.aforament, 0);
  }

  get entradesVenudes() {
    return this.#sessions.reduce((total, s) => total + s.venudes, 0);
  }

  get ocupacio() {
    if (this.aforamentTotal === 0) return 0;
    return Math.round((this.entradesVenudes / this.aforamentTotal) * 100);
  }

  // Metode privat: detall intern que no forma part de l'API publica
  #cercarSessio(idSessio) {
    return this.#sessions.find((s) => s.id === idSessio);
  }

  entradesLliures(idSessio) {
    const sessio = this.#cercarSessio(idSessio);
    if (!sessio) return 0;
    return sessio.aforament - sessio.venudes;
  }

  reservar(idSessio, quantitat = 1) {
    const sessio = this.#cercarSessio(idSessio);

    if (!sessio) {
      throw new Error(`La sessio ${idSessio} no existeix a ${this.id}`);
    }
    if (sessio.aforament - sessio.venudes < quantitat) {
      throw new Error(`Aforament insuficient a ${idSessio}`);
    }

    sessio.venudes += quantitat;
    return sessio.venudes;
  }

  // toString s'invoca automaticament en interpolar en una plantilla
  toString() {
    return `${this.titol} (${this.sala}) - ${this.ocupacio}% ocupat`;
  }

  // Metode estatic: pertany a la classe, no a cada instancia
  static desDeJSON(objecte) {
    return new Esdeveniment(objecte);
  }
}

Ús:

const concert = Esdeveniment.desDeJSON(cataleg[0]);

console.log(concert.nombreSessions);              // 2   (getter, sense parentesis)
console.log(concert.ocupacio);                    // 33
console.log(concert.entradesLliures('ses-001-1')); // 240
console.log(`${concert}`);                        // Concierto de Otono (Teatro Almendra) - 33% ocupat

concert.reservar('ses-001-1', 4);
console.log(concert.entradesLliures('ses-001-1')); // 236

// Els camps privats son realment inaccessibles des de fora
// console.log(concert.#sessions);  // SyntaxError en temps de compilacio

Sobre els camps privats #:

  • Són privacitat real, aplicada pel motor, no una convenció com el prefix _.
  • Només s'hi pot accedir dins del cos de la classe.
  • No apareixen a Object.keys(), ni a JSON.stringify(), ni en inspeccionar l'objecte.
  • Es poden comprovar amb #camp in objecte per saber si una instància és d'aquella classe.

  1. JSON: parse i stringify

JSON és el format d'intercanvi de tot el curs: el fitxer llavor dades/esdeveniments.json, les respostes de l'API, la configuració. JavaScript el maneja de manera nativa.

// D'objecte a text
const text = JSON.stringify(cataleg[0]);
// {"id":"evt-001","titol":"Concierto de Otono",...}   tot en una linia

// Amb format llegible: el tercer argument es la indentacio
const textLlegible = JSON.stringify(cataleg[0], null, 2);
console.log(textLlegible);
{
  "id": "evt-001",
  "titol": "Concierto de Otono",
  "sala": "Teatro Almendra",
  "sessions": [
    {
      "id": "ses-001-1",
      "aforament": 420
    }
  ]
}

Fes servir sempre null, 2 per a fitxers que hagi de llegir una persona (com dades/esdeveniments.json): el control de versions mostrarà diferències línia a línia en comptes d'una única línia gegant modificada.

El segon argument (null a dalt) és un filtre, molt útil per seleccionar camps:

// Nomes aquests camps a la sortida
JSON.stringify(cataleg[0], ['id', 'titol', 'sala'], 2);

// O una funcio, per transformar o excloure valors
JSON.stringify(usuari, (clau, valor) => (clau === 'hashContrasenya' ? undefined : valor), 2);

De text a objecte:

const recuperat = JSON.parse(textLlegible);
console.log(recuperat.titol);   // Concierto de Otono

JSON.parse llança una excepció si el text no és JSON vàlid. Embolcalla'l sempre en try/catch quan l'origen no sigui de confiança:

function llegirCatalegSegur(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    console.error(`JSON invalid: ${error.message}`);
    return [];
  }
}

Limitacions que has de conèixer:

Tipus en JavaScript En passar per JSON.stringify
Date Es converteix en cadena ISO; en tornar és text, no Date
undefined Desapareix dels objectes; es converteix en null als arrays
Funció Desapareix
Map, Set Es converteixen en {} — cal transformar-los a mà
NaN, Infinity Es converteixen en null
BigInt Llança TypeError
Referència circular Llança TypeError

Que les dates es converteixin en text és una altra raó per a la nostra convenció: desem les dates com a cadenes ISO des del principi, així el viatge d'anada i tornada per JSON no perd res.

Errors Comuns i Consells

Error 1: fer servir || per a valors per defecte numèrics. sessio.venudes || 100 retorna 100 quan hi ha 0 venudes. Fes servir ??.

Error 2: creure que const congela l'objecte. const protegeix la variable, no el seu contingut. Fes servir Object.freeze si necessites immutabilitat.

Error 3: oblidar el return a reduce. És l'errada més freqüent. Si el resultat és undefined, revisa això primer.

Error 4: cridar sort sense copiar. Modifica l'array original i provoca errades difícils de rastrejar. [...array].sort(...).

Error 5: sort() sense comparador sobre nombres. Ordena com a text: [10, 9] queda [10, 9]. Passa sempre (a, b) => a - b.

Error 6: retorn implícit d'un objecte en una fletxa. (s) => { id: s.id } retorna undefined. Embolcalla entre parèntesis: (s) => ({ id: s.id }).

Error 7: creure que la propagació copia en profunditat. { ...esdeveniment } comparteix l'array sessions amb l'original. Fes servir structuredClone.

Consell 1: encadena, però amb mesura. Tres o quatre mètodes encadenats es llegeixen molt bé; deu, no. Extreu passos a variables amb nom.

Consell 2: desestructura als paràmetres. Deixa clar què necessita cada funció i documenta la signatura sense escriure ni un sol comentari.

Consell 3: prototipa al REPL. Totes les transformacions d'aquesta lliçó es poden provar en segons amb .load src/cataleg-dades.js.

Consell 4: prefereix mètodes que no modifiquin. map, filter i la propagació retornen coses noves; el codi resultant és molt més fàcil de raonar.

Exercicis

Exercici 1: informe de sales

Escriu una funció informeSales(cataleg) que retorni un array d'objectes amb aquesta forma, ordenat de major a menor ocupació:

[
  { sala: 'Auditorio Ribera', esdeveniments: 1, sessions: 2, aforament: 1800, venudes: 1360, ocupacio: 76 },
  ...
]

Requisits: fes servir reduce per agrupar, Object.entries per recórrer el resultat i sort sobre una còpia. Mostra el resultat amb console.table.

Exercici 2: la classe Sessio

Crea una classe Sessio amb:

  • Constructor que rebi un objecte desestructurat (id, dataHora, aforament, venudes, preuCentims).
  • Un camp privat #venudes.
  • Getters lliures, ocupacio (enter de 0 a 100), exhaurida (booleà) i preuEuros (cadena amb dos decimals).
  • Mètode vendre(quantitat = 1) que llanci un Error si no hi ha aforament suficient i retorni el nombre d'entrades venudes després de l'operació.
  • Mètode toJSON() que retorni un objecte pla llest per a JSON.stringify (incloent-hi venudes, que és privat).

Prova-la amb ses-002-1 (120 d'aforament, 118 venudes): ven 2 entrades i comprova que queda exhaurida; després intenta vendre'n una més i captura l'error.

Exercici 3: catàleg públic

Escriu catalegPublic(cataleg, opcions) que prepari les dades per enviar-les a un client web. Ha de:

  1. Acceptar opcions = { sala, preuMaximCentims, nomesDisponibles = true }, totes opcionals, amb desestructuració i valors per defecte.
  2. Filtrar per sala si s'indica.
  3. Descartar les sessions exhaurides si nomesDisponibles és true.
  4. Descartar les sessions que superin preuMaximCentims si s'indica.
  5. Eliminar del resultat els esdeveniments que s'hagin quedat sense cap sessió.
  6. Retornar els esdeveniments sense el camp categoria i amb les sessions transformades a { id, dataHora, lliures, preuEuros }.
  7. Imprimir el resultat amb JSON.stringify(resultat, null, 2).

Solucions

Solució 1

function informeSales(cataleg) {
  // 1. Agrupem per sala amb reduce.
  const agrupat = cataleg.reduce((acumulat, esdeveniment) => {
    // ??= crea l'entrada la primera vegada que apareix la sala.
    acumulat[esdeveniment.sala] ??= { esdeveniments: 0, sessions: 0, aforament: 0, venudes: 0 };

    const registre = acumulat[esdeveniment.sala];
    registre.esdeveniments += 1;
    registre.sessions += esdeveniment.sessions.length;

    for (const sessio of esdeveniment.sessions) {
      registre.aforament += sessio.aforament;
      registre.venudes += sessio.venudes;
    }

    return acumulat;   // Imprescindible
  }, {});

  // 2. Passem d'objecte a array afegint el nom de la sala i l'ocupacio.
  const files = Object.entries(agrupat).map(([sala, dades]) => ({
    sala,
    ...dades,
    ocupacio: Math.round((dades.venudes / dades.aforament) * 100)
  }));

  // 3. Ordenem sobre una copia (files ja es un array nou, encara que mantenim l'habit).
  return [...files].sort((a, b) => b.ocupacio - a.ocupacio);
}

console.table(informeSales(cataleg));
┌─────────┬────────────────────┬───────────────┬──────────┬───────────┬─────────┬──────────┐
│ (index) │ sala               │ esdeveniments │ sessions │ aforament │ venudes │ ocupacio │
├─────────┼────────────────────┼───────────────┼──────────┼───────────┼─────────┼──────────┤
│ 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ó 2

class Sessio {
  #venudes = 0;

  constructor({ id, dataHora, aforament, venudes = 0, preuCentims }) {
    this.id = id;
    this.dataHora = dataHora;
    this.aforament = aforament;
    this.preuCentims = preuCentims;
    this.#venudes = venudes;
  }

  get venudes() {
    return this.#venudes;
  }

  get lliures() {
    return this.aforament - this.#venudes;
  }

  get ocupacio() {
    return Math.round((this.#venudes / this.aforament) * 100);
  }

  get exhaurida() {
    return this.lliures === 0;
  }

  get preuEuros() {
    return (this.preuCentims / 100).toFixed(2);
  }

  vendre(quantitat = 1) {
    if (!Number.isInteger(quantitat) || quantitat < 1) {
      throw new Error('La quantitat ha de ser un enter positiu');
    }
    if (quantitat > this.lliures) {
      throw new Error(`Aforament insuficient a ${this.id}: en queden ${this.lliures}`);
    }

    this.#venudes += quantitat;
    return this.#venudes;
  }

  // JSON.stringify crida automaticament toJSON si existeix.
  toJSON() {
    return {
      id: this.id,
      dataHora: this.dataHora,
      aforament: this.aforament,
      venudes: this.#venudes,
      preuCentims: this.preuCentims
    };
  }
}

// Prova
const sessio = new Sessio({
  id: 'ses-002-1',
  dataHora: '2026-10-10T21:30:00',
  aforament: 120,
  venudes: 118,
  preuCentims: 1800
});

console.log(sessio.lliures);       // 2
sessio.vendre(2);
console.log(sessio.exhaurida);     // true
console.log(sessio.ocupacio);      // 100
console.log(sessio.preuEuros);     // 18.00

try {
  sessio.vendre();
} catch (error) {
  console.error(`No s'ha pogut vendre: ${error.message}`);
  // No s'ha pogut vendre: Aforament insuficient a ses-002-1: en queden 0
}

console.log(JSON.stringify(sessio, null, 2));   // Inclou "venudes": 120

Sense toJSON(), el camp privat #venudes no apareixeria a la serialització: és exactament el cas d'ús per al qual existeix aquest mètode.

Solució 3

function catalegPublic(
  cataleg,
  { sala = null, preuMaximCentims = Infinity, nomesDisponibles = true } = {}
) {
  return cataleg
    // 2. Filtre per sala (si no s'indica, passen tots).
    .filter((esdeveniment) => sala === null || esdeveniment.sala === sala)

    // 3, 4 i 6. Transformem cada esdeveniment i les seves sessions.
    .map((esdeveniment) => {
      // Traiem "categoria" amb desestructuracio + resta.
      const { categoria, sessions, ...dadesPubliques } = esdeveniment;

      const sessionsFiltrades = sessions
        .filter((s) => !nomesDisponibles || s.venudes < s.aforament)
        .filter((s) => s.preuCentims <= preuMaximCentims)
        .map((s) => ({
          id: s.id,
          dataHora: s.dataHora,
          lliures: s.aforament - s.venudes,
          preuEuros: (s.preuCentims / 100).toFixed(2)
        }));

      return { ...dadesPubliques, sessions: sessionsFiltrades };
    })

    // 5. Fora els esdeveniments que s'hagin quedat sense sessions.
    .filter((esdeveniment) => esdeveniment.sessions.length > 0);
}

// Prova: entrades de fins a 20 EUR a la Sala Boveda
const resultat = catalegPublic(cataleg, {
  sala: 'Sala Boveda',
  preuMaximCentims: 2000
});

console.log(JSON.stringify(resultat, null, 2));
[
  {
    "id": "evt-002",
    "titol": "Noche de Monologos",
    "sala": "Sala Boveda",
    "duracioMinuts": 80,
    "sessions": [
      { "id": "ses-002-1", "dataHora": "2026-10-10T21:30:00", "lliures": 2, "preuEuros": "18.00" },
      { "id": "ses-002-2", "dataHora": "2026-10-11T21:30:00", "lliures": 75, "preuEuros": "18.00" },
      { "id": "ses-002-3", "dataHora": "2026-10-17T21:30:00", "lliures": 108, "preuEuros": "15.00" }
    ]
  }
]

Fixa't com const { categoria, sessions, ...dadesPubliques } = esdeveniment compleix dues funcions alhora: extreu el que hem de tractar a part i elimina categoria del resultat sense escriure ni un sol delete.

Conclusió

Aquest repàs ha recorregut el JavaScript modern que faràs servir a cada línia de la resta del curs, sempre resolent problemes reals del catàleg: const/let i l'àmbit de bloc, funcions fletxa i el comportament de this, desestructuració i paràmetres per defecte com a manera de rebre opcions, el doble ús de l'operador ... per copiar i per recollir, ?. i ?? per tractar dades incompletes sense rebentar i sense esborrar els zeros legítims, plantilles de cadena, l'arsenal de mètodes d'array —amb especial atenció a reduce i als paranys de sort—, Object.entries per recórrer registres, Map i Set per a índexs i col·leccions úniques, classes amb getters i camps privats #, i JSON amb el seu null, 2 obligatori i les seves limitacions de tipus.

Han quedat fixades pel camí diverses decisions del projecte que ja no canviaran: preus en cèntims enters, dates com a cadenes ISO (que a més s'ordenen bé com a text) i objectes d'opcions amb valors per defecte com a signatura habitual de les nostres funcions.

Falta l'altra meitat del JavaScript de servidor: l'asincronia. Promeses, async/await i l'ordre real en què s'executen les coses són l'eix del Mòdul 2, i és allà on el model de la primera lliçó —el cambrer que no espera— es converteix en codi.

Abans d'això, tanquem el mòdul amb la lliçó que dona sentit a tot: El Projecte del Curs: la Plataforma Escena Viva. Formalitzarem el model de domini, escriurem el fitxer llavor dades/esdeveniments.json que substituirà les dades incrustades al codi, fixarem les convencions del projecte i veuràs el full de ruta complet: què construiràs a cadascun dels dotze mòduls i com encaixa cada peça a la plataforma final.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats