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
const,leti l'àmbit de bloc- Funcions fletxa i el valor de
this - Desestructuració, paràmetres per defecte i propietats abreujades
- Propagació i resta: l'operador
... - Encadenament opcional
?.i coalescència nul·la?? - Plantilles de cadena
- Mètodes d'array essencials aplicats al catàleg
- Objectes:
Object.keys,valuesientries MapiSet: quan fer servir cadascun- Classes, camps i mètodes privats
- JSON:
parseistringify
const, let i l'àmbit de bloc
const, let i l'àmbit de blocProblema: 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 variableSi vols impedir també la modificació, existeix Object.freeze(sessio).
- Funcions fletxa i el valor de
this
thisProblema: 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 implicitCompte 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 }); // CorrecteEl 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) |
- 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 03Parà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 baratesAquesta 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);
}
};
- 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); // 1200L'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
- Encadenament opcional
?. i coalescència nul·la ??
?. 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:
Existeix també l'operador d'assignació ??=:
const recompte = {};
recompte['Sala Boveda'] ??= 0; // Nomes assigna si es null o undefined
recompte['Sala Boveda'] += 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'}`);
- 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); // 7reduce 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); // 3000Pas 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 elreturn acumulatdins de la funció. El resultat seràundefineda 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); // falseTots 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.
- Objectes:
Object.keys, values i entries
Object.keys, values i entriesProblema: 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`);
}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
Map i Set: quan fer servir cadascun
Map i Set: quan fer servir cadascunProblema: 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).
- 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 compilacioSobre 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 aJSON.stringify(), ni en inspeccionar l'objecte. - Es poden comprovar amb
#camp in objecteper saber si una instància és d'aquella classe.
- JSON:
parse i stringify
parse i stringifyJSON é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:
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à) ipreuEuros(cadena amb dos decimals). - Mètode
vendre(quantitat = 1)que llanci unErrorsi 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 aJSON.stringify(incloent-hivenudes, 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:
- Acceptar
opcions = { sala, preuMaximCentims, nomesDisponibles = true }, totes opcionals, amb desestructuració i valors per defecte. - Filtrar per sala si s'indica.
- Descartar les sessions exhaurides si
nomesDisponibleséstrue. - Descartar les sessions que superin
preuMaximCentimssi s'indica. - Eliminar del resultat els esdeveniments que s'hagin quedat sense cap sessió.
- Retornar els esdeveniments sense el camp
categoriai amb les sessions transformades a{ id, dataHora, lliures, preuEuros }. - 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": 120Sense 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
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
