La lliçó anterior va acabar amb una pregunta oberta: quan crearTasca() fa ultimId = ultimId + 1, està tocant una variable que viu fora d'ella. Per què la pot veure? I es podria tenir un comptador d'identificadors que ningú més no pogués tocar per accident, sense deixar-lo solt al fitxer? Respondre-ho exigeix entendre dos conceptes que són entre els més importants de JavaScript: l'àmbit (on viu i des d'on es veu cada variable) i els closures (la capacitat d'una funció de recordar l'entorn on va néixer). Amb ells podràs crear estat privat, fàbriques de funcions configurades i memòries cau de resultats, tres eines que faràs servir a Nómada Tasques fins al final del curs.

Contingut

  1. Què és l'àmbit
  2. Àmbit global
  3. Àmbit de funció
  4. Àmbit de bloc: let i const davant de var
  5. La cadena d'àmbits, pas a pas
  6. Ombrejat de variables (shadowing)
  7. Què és un closure
  8. El closure explicat amb un exemple mínim
  9. El clàssic bucle amb var davant de let
  10. Patró 1: generador d'identificadors
  11. Patró 2: fàbrica de funcions configurades
  12. Patró 3: estat privat amb el patró mòdul
  13. Patró 4: memoïtzació senzilla
  14. El cost en memòria
  15. Errors Habituals i Consells
  16. Exercicis
  17. Conclusió

  1. Què és l'àmbit

L'àmbit (scope) d'una variable és la regió del codi des de la qual se'n pot fer servir el nom. No és un detall acadèmic: és el que determina si una línia funciona o llança ReferenceError.

JavaScript té tres tipus d'àmbit:

Àmbit Es crea amb Viu mentre El declaren
Global El fitxer o el <script> Duri el programa const/let/var fora de tot
De funció Cada crida a una funció Duri la crida Paràmetres i qualsevol declaració interna
De bloc Cada { } (if, for, bloc solt) Duri el bloc Només const i let (no var)

I una regla que governa tota la resta:

Des de dins es veu cap a fora; des de fora no es veu cap a dins.

  1. Àmbit global

Tot el que es declara fora de qualsevol funció i de qualsevol bloc és global: visible des de qualsevol punt del fitxer.

'use strict';

const AVUI = '2026-09-20';       // global: constant de configuració del projecte
const LIMIT_SETMANAL = 40;       // global: regla R7

function margeDe(horesObertes) {
  return LIMIT_SETMANAL - horesObertes;    // ✓ veu la global
}

console.log(margeDe(25));   // 15

Les globals són útils per a constants de configuració i perilloses per a tota la resta:

Risc Explicació
Col·lisions de nom Dos fitxers que declaren tasques es trepitgen
Acoblament invisible Una funció que llegeix una global no diu a la seva signatura de què depèn
Impossible de provar No la pots executar amb altres valors sense modificar la global
Mutació des de qualsevol lloc Qualsevol línia del programa la pot canviar

Regla pràctica: globals només en const i només per a configuració. Tot el que sigui variable, dins de funcions o protegit per un closure, com veuràs a l'apartat 12.

  1. Àmbit de funció

Cada crida a una funció crea un àmbit nou. Els paràmetres i les variables declarades al seu cos existeixen allà i desapareixen en acabar.

function resumirResponsable(nom, hores) {
  const limit = 40;                        // local
  const marge = limit - hores;             // local
  return `${nom}: marge de ${marge} h`;
}

console.log(resumirResponsable('Iván', 25));   // Iván: marge de 15 h
console.log(limit);                            // ✗ ReferenceError: limit is not defined

I una cosa important: dues crides no comparteixen res.

function comptarDesDeZero() {
  let comptador = 0;     // neix de nou a cada crida
  comptador++;
  return comptador;
}

console.log(comptarDesDeZero());   // 1
console.log(comptarDesDeZero());   // 1  ← no recorda res

Aquesta amnèsia és el que els closures de l'apartat 7 permeten trencar quan convé.

  1. Àmbit de bloc: let i const davant de var

Un bloc és qualsevol parell de claus: el cos d'un if, d'un for, d'un while o unes claus soltes. let i const es queden dins del bloc; var no.

function analitzarTasca(estat, hores) {
  if (estat === 'feta') {
    const missatge = 'Tasca completada';   // només dins de l'if
    var registre = 'feta';                 // s'escapa a tota la funció!
  }

  console.log(registre);   // 'feta'       ← var ignora el bloc
  console.log(missatge);   // ✗ ReferenceError
}

La comparació completa de les tres maneres de declarar:

var let const
Àmbit Funció Bloc Bloc
Es pot reassignar No
Es pot redeclarar al mateix àmbit (perillós) No No
Abans de la línia de declaració undefined Error (TDZ) Error (TDZ)
S'afegeix a globalThis si és global No No
Recomanació No fer-la servir Quan canviï Per defecte

Aquest "es pot redeclarar" de var és especialment traïdor:

var responsable = 'Iván';
var responsable = 'Marta';    // ✓ sense error: has perdut el valor anterior sense adonar-te

let revisor = 'Lucía';
let revisor = 'Marta';        // ✗ SyntaxError: Identifier 'revisor' has already been declared

En aquest curs no faràs servir var. Apareix aquí només perquè el veuràs en codi antic i perquè el seu comportament explica l'exercici clàssic de l'apartat 9.

  1. La cadena d'àmbits, pas a pas

Quan el motor troba un nom, el busca a l'àmbit actual. Si no hi és, puja a l'àmbit que el conté. I així successivament fins al global. Si tampoc no hi és: ReferenceError.

'use strict';

const AVUI = '2026-09-20';                      // ── Àmbit global

function generarInforme(responsable) {          // ── Àmbit de generarInforme
  const capcalera = `Informe de ${responsable}`;

  function detallar(hores) {                    // ── Àmbit de detallar
    const linia = `${hores} h`;
    return `${capcalera} · ${linia} · a ${AVUI}`;
    //       ↑ àmbit pare   ↑ local  ↑ global
  }

  return detallar(25);
}

console.log(generarInforme('Iván'));
// Informe de Iván · 25 h · a 2026-09-20

Així busca el motor cada nom dins de detallar:

flowchart TD
    A["Àmbit de detallar<br/>linia, hores"] -->|no troba 'capcalera'| B["Àmbit de generarInforme<br/>capcalera, responsable"]
    B -->|no troba 'AVUI'| C["Àmbit global<br/>AVUI, generarInforme"]
    C -->|no troba res| D["ReferenceError"]

Tres conseqüències que convé fixar:

  1. La cerca és unidireccional: cap amunt. Des de generarInforme no es pot veure linia.
  2. S'atura a la primera coincidència. Si detallar declarés la seva pròpia capcalera, la cerca s'aturaria allà (apartat 6).
  3. La cadena es decideix en escriure el codi, no en executar-lo. Això s'anomena àmbit lèxic, i és la clau per entendre els closures: el que importa és on està escrita la funció, no des d'on es crida.

  1. Ombrejat de variables (shadowing)

Quan un àmbit intern declara una variable amb el mateix nom que una d'externa, la interna tapa l'externa dins de la seva regió.

const estat = 'global';

function inspeccionar() {
  const estat = 'de funció';

  if (true) {
    const estat = 'de bloc';
    console.log(estat);       // de bloc
  }

  console.log(estat);         // de funció
}

inspeccionar();
console.log(estat);           // global

L'ombrejat és legal i de vegades útil (un paràmetre anomenat tasca dins d'una funció que treballa amb tasques), però també és una font de confusió. Un cas real que mossega:

const LIMIT_SETMANAL = 40;

function comprovarCarrega(hores, LIMIT_SETMANAL) {   // ✗ el paràmetre ombreja la constant
  return hores > LIMIT_SETMANAL;
}

console.log(comprovarCarrega(45));   // false  ← LIMIT_SETMANAL és undefined a dins

Qui llegeix el cos assumeix que LIMIT_SETMANAL val 40, però a dins val undefined perquè no s'ha passat el segon argument, i 45 > undefined és false. Regles per evitar-ho:

  • No reutilitzis noms de constants globals com a paràmetres.
  • Si el linter avisa de no-shadow, fes-li cas (08-02).
  • Quan l'ombrejat sigui deliberat, que es noti: tasca a fora i tascaOriginal a dins, per exemple.

  1. Què és un closure

Un closure (o clausura) és una funció juntament amb l'entorn de variables en què va ser creada. Dit d'una altra manera:

Quan una funció es defineix dins d'una altra, recorda les variables de la funció exterior, fins i tot després que aquesta funció exterior hagi acabat.

La part sorprenent és l'última. Ja saps que les variables d'una funció desapareixen en acabar la crida. Però si una funció interna les continua fent servir, no desapareixen: el motor les manté vives tot el temps que calgui.

flowchart LR
    subgraph E["Entorn capturat (sobreviu)"]
        V["comptador = 0"]
    end
    F["funció interna<br/>retornada a l'exterior"] --> V
    C["qui la crida<br/>des de fora"] --> F

  1. El closure explicat amb un exemple mínim

Comencem pel cas més petit possible:

function crearSalutacio(nom) {
  const salutacio = `Hola, ${nom}`;      // viu a l'àmbit de crearSalutacio

  return function () {                   // funció interna
    return salutacio;                    // fa servir una variable de l'àmbit pare
  };
}

const saludarMarta = crearSalutacio('Marta');

// crearSalutacio JA ha acabat... i tanmateix:
console.log(saludarMarta());   // Hola, Marta

Llegeix bé la seqüència:

  1. Es crida crearSalutacio('Marta') i es crea un àmbit amb nom i salutacio.
  2. Es crea la funció interna, que referencia salutacio.
  3. crearSalutacio acaba i retorna aquesta funció.
  4. Normalment salutacio desapareixeria. Però com que la funció retornada la necessita, el motor conserva l'entorn.
  5. En cridar saludarMarta(), la funció troba la seva salutacio intacta.

I cada crida a crearSalutacio crea un entorn independent:

const saludarIvan  = crearSalutacio('Iván');
const saludarLucia = crearSalutacio('Lucía');

console.log(saludarIvan());    // Hola, Iván
console.log(saludarLucia());   // Hola, Lucía

Dues funcions idèntiques en el seu codi, amb memòries diferents. Aquest és el mecanisme complet. Tot el que ve després són aplicacions d'aquesta mateixa idea.

  1. El clàssic bucle amb var davant de let

Aquest exercici apareix a totes les entrevistes de feina i, més important, explica un error real que veuràs al Mòdul 6 en assignar gestors d'esdeveniments dins d'un bucle.

const titols = ['Redissenyar la sala', 'Cartelleria', 'Web de reserves'];
const accions = [];

for (var i = 0; i < titols.length; i++) {
  accions.push(function () {
    console.log(`${i}: ${titols[i]}`);
  });
}

accions[0]();   // 3: undefined
accions[1]();   // 3: undefined
accions[2]();   // 3: undefined

Per què? Perquè var iàmbit de funció, no de bloc: hi ha una sola i per a tot el bucle. Les tres funcions capturen la mateixa variable, i quan s'executen (després del bucle) aquesta i ja val 3.

Amb let, cada iteració crea la seva pròpia i:

for (let i = 0; i < titols.length; i++) {
  accions.push(function () {
    console.log(`${i}: ${titols[i]}`);
  });
}

accions[0]();   // 0: Redissenyar la sala
accions[1]();   // 1: Cartelleria
accions[2]();   // 2: Web de reserves
flowchart TD
    subgraph VAR["Amb var: una sola variable"]
        A1["fn 0"] --> I["i = 3"]
        A2["fn 1"] --> I
        A3["fn 2"] --> I
    end
    subgraph LET["Amb let: una per iteració"]
        B1["fn 0"] --> J1["i = 0"]
        B2["fn 1"] --> J2["i = 1"]
        B3["fn 2"] --> J3["i = 2"]
    end

Abans de let, la solució era embolcallar cada iteració en una IIFE —el patró que vas veure a Expressions de Funció i Funcions Fletxa— precisament per crear un àmbit nou a cada volta:

for (var i = 0; i < titols.length; i++) {
  (function (index) {
    accions.push(function () { console.log(`${index}: ${titols[index]}`); });
  })(i);
}

Avui n'hi ha prou d'escriure let. Però ara ja saps per què.

  1. Patró 1: generador d'identificadors

Tornem al cap solt de la lliçó anterior. ultimId estava solta al fitxer, visible i modificable per qualsevol. Amb un closure, deixa d'existir per a la resta del programa.

'use strict';

/**
 * Retorna una funció que produeix identificadors correlatius (R1).
 * El comptador queda tancat: ningú de fora no el pot llegir ni canviar.
 */
function crearGeneradorIds(inicial = 0) {
  let ultimId = inicial;

  return function seguentId() {
    ultimId = ultimId + 1;
    return ultimId;
  };
}

const seguentId = crearGeneradorIds(6);   // el backlog ja té 6 tasques

console.log(seguentId());     // 7
console.log(seguentId());     // 8
console.log(seguentId());     // 9

console.log(typeof ultimId);  // 'undefined'  ← protegida

Compara les dues versions:

Variable global ultimId Closure crearGeneradorIds
Qui la pot modificar Qualsevol línia del programa Només la funció retornada
Risc que un altre fitxer la trepitgi Alt Cap
Diversos generadors independents Impossible crearGeneradorIds() un altre cop
Valor inicial configurable Editant el fitxer Per paràmetre

Aquest darrer punt és molt útil a les proves: al Mòdul 8 podràs crear un generador que comenci a 0 per a cada test, sense que un test contamini el següent.

const idsDeProva = crearGeneradorIds();
console.log(idsDeProva());   // 1  ← independent del generador de producció
console.log(seguentId());    // 10 ← l'altre continua pel seu compte

  1. Patró 2: fàbrica de funcions configurades

Una fàbrica és una funció que retorna una altra funció ja configurada amb uns paràmetres fixos. Evita repetir aquests paràmetres a cada crida.

const responsables = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const titols       = [
  'Redissenyar la sala polivalent',
  'Cartelleria del taller de serigrafia',
  'Actualitzar el web de reserves',
  'Inventari de tintes de serigrafia',
  'Guia sobre enquadernació per a residents',
  'Pressupost de la fusteria'
];
const hores  = [12, 6, 14, 3, 8, 5];
const estats = ['en-curs', 'pendent', 'pendent', 'feta', 'en-curs', 'pendent'];

/**
 * Retorna una funció que diu si la tasca de l'índex i pertany a `nom`.
 * `nom` queda capturat al closure.
 */
function crearFiltrePerResponsable(nom) {
  return function (index) {
    return responsables[index] === nom;
  };
}

const esDeIvan  = crearFiltrePerResponsable('Iván');
const esDeMarta = crearFiltrePerResponsable('Marta');

console.log(esDeIvan(0));    // true
console.log(esDeIvan(1));    // false
console.log(esDeMarta(1));   // true

I així es fa servir per recórrer el backlog amb criteris intercanviables:

function llistarSegons(filtre) {
  const trobades = [];
  for (let i = 0; i < titols.length; i++) {
    if (filtre(i)) trobades.push(titols[i]);
  }
  return trobades;
}

console.log(llistarSegons(esDeIvan));
// [ 'Redissenyar la sala polivalent',
//   'Guia sobre enquadernació per a residents',
//   'Pressupost de la fusteria' ]

// I les fàbriques es poden combinar
function crearFiltrePerEstat(estat) {
  return (index) => estats[index] === estat;
}

function crearFiltreCombinat(filtreA, filtreB) {
  return (index) => filtreA(index) && filtreB(index);
}

const pendentsDeIvan = crearFiltreCombinat(esDeIvan, crearFiltrePerEstat('pendent'));
console.log(llistarSegons(pendentsDeIvan));
// [ 'Pressupost de la fusteria' ]

Fixa't en el que acaba de passar: llistarSegons no sap res de responsables ni d'estats; només sap cridar una funció que li diuen. Això és una funció d'ordre superior, i és el tema complet de Funcions d'Ordre Superior.

  1. Patró 3: estat privat amb el patró mòdul

El patró mòdul combina un closure amb un objecte de retorn: les variables queden privades i només s'exposa un conjunt controlat de funcions.

'use strict';

function crearMagatzemTasques(idInicial = 0) {
  // ── Estat privat: inaccessible des de fora ───────────────────
  const tasques = [];
  let ultimId = idInicial;

  // ── Funció auxiliar privada ──────────────────────────────────
  function cercarIndex(id) {
    for (let i = 0; i < tasques.length; i++) {
      if (tasques[i].id === id) return i;
    }
    return -1;
  }

  // ── Interfície pública ───────────────────────────────────────
  return {
    afegir: function (titol, responsable, horesEstimades) {
      ultimId = ultimId + 1;
      tasques.push({
        id: ultimId,
        titol: titol,
        responsable: responsable ?? null,
        estat: 'pendent',
        horesEstimades: horesEstimades
      });
      return ultimId;
    },

    canviarEstat: function (id, nouEstat) {
      const i = cercarIndex(id);
      if (i === -1) throw new Error(`No existeix la tasca ${id}.`);
      tasques[i].estat = nouEstat;
      return true;
    },

    comptar: function () {
      return tasques.length;
    },

    horesObertes: function () {
      let total = 0;
      for (const t of tasques) {
        if (t.estat !== 'feta') total += t.horesEstimades;
      }
      return total;
    }
  };
}

const magatzem = crearMagatzemTasques(6);

console.log(magatzem.afegir('Renovar el torn', 'Lucía', 10));   // 7
console.log(magatzem.afegir('Comprar tintes', null, 2));        // 8
console.log(magatzem.comptar());                                // 2
console.log(magatzem.horesObertes());                           // 12

magatzem.canviarEstat(7, 'feta');
console.log(magatzem.horesObertes());                           // 2

console.log(magatzem.tasques);    // undefined  ← protegit
console.log(typeof cercarIndex);  // 'undefined' ← privada

Les garanties que ofereix aquesta estructura són reals, no d'estil:

  • Ningú no pot fer magatzem.tasques.push(escombraries), perquè tasques no està exposat.
  • ultimId no es pot alterar, així que la R1 (identificadors únics i correlatius) està garantida per construcció.
  • cercarIndex és un detall intern: es pot reescriure sense trencar res a ningú.

Al Mòdul 5 veuràs dues evolucions d'aquesta idea: les classes amb camps privats i els mòduls ES, que aconsegueixen el mateix a escala de fitxer. El patró mòdul amb closures és l'antecessor de tots dos i continua sent perfectament vàlid.

  1. Patró 4: memoïtzació senzilla

Memoïtzar és desar el resultat d'una funció per no haver-lo de tornar a calcular amb els mateixos arguments. El magatzem de resultats viu en un closure.

/**
 * Compta les tasques d'un responsable, desant a la memòria cau cada resultat.
 * Només és correcte perquè comptar és una funció PURA (vegeu 03-03).
 */
function crearComptadorMemoitzat(responsables) {
  const cache = {};      // privat: la memòria de la funció
  let calculs = 0;
  let encerts = 0;

  const comptar = function (nom) {
    if (nom in cache) {
      encerts++;
      return cache[nom];
    }

    calculs++;
    let total = 0;
    for (const r of responsables) {
      if (r === nom) total++;
    }
    cache[nom] = total;
    return total;
  };

  comptar.estadistiques = () => `${calculs} càlculs, ${encerts} encerts de memòria cau`;
  return comptar;
}

const comptarTasquesDe = crearComptadorMemoitzat(responsables);

console.log(comptarTasquesDe('Iván'));    // 3  (calculat)
console.log(comptarTasquesDe('Iván'));    // 3  (de memòria cau)
console.log(comptarTasquesDe('Marta'));   // 2  (calculat)
console.log(comptarTasquesDe('Iván'));    // 3  (de memòria cau)
console.log(comptarTasquesDe.estadistiques());   // 2 càlculs, 2 encerts de memòria cau

Dos avisos importants sobre la memoïtzació:

  1. Només és vàlida amb funcions pures. Si el resultat depèn d'alguna cosa que canvia (el rellotge, una variable externa, el contingut del backlog), la memòria cau retornarà dades obsoletes. Per això 03-03 insistia tant en la puresa.
  2. La memòria cau creix. Cada argument nou hi afegeix una entrada que no s'allibera mai mentre la funció visqui.

A Recursivitat aplicaràs exactament aquesta tècnica a Fibonacci, on la diferència de rendiment és espectacular.

  1. El cost en memòria

Un closure manté viu tot l'entorn de la funció exterior mentre la funció interna existeixi. Això és potent i té preu:

function crearFormatadorPesat() {
  const backlogComplet = carregarMilersDeTasques();   // imagina 50 MB de dades
  const separador = ' · ';

  return function (titol, hores) {
    return titol + separador + hores;                 // només fa servir `separador`
  };
}

const formatar = crearFormatadorPesat();
// `backlogComplet` pot continuar en memòria mentre `formatar` existeixi

Els motors moderns optimitzen força bé aquest cas i solen alliberar el que la funció interna demostradament no fa servir, però no convé dependre'n. Bons hàbits:

Pràctica Motiu
Capturar només el necessari (extreure la dada abans de tancar-hi a sobre) Redueix el que queda retingut
Posar a null les referències grans quan ja no calguin Permet alliberar-les
Limitar la mida de les memòries cau de memoïtzació Evita creixement sense fi
No crear closures dins de bucles molt llargs si no cal Cadascun és un entorn viu

Això connecta directament amb les fuites de memòria de Gestió de Memòria, on veuràs com detectar-les amb les DevTools. Per ara n'hi ha prou amb la idea: un closure no és gratis, però en el 99 % dels casos el seu cost és irrellevant comparat amb el que aporta.

Errors Habituals i Consells

1. Creure que un closure copia el valor. No el copia: manté una referència viva a la variable.

function crearEtiquetador() {
  let prefix = 'TN';
  const etiquetar = (id) => `${prefix}-${id}`;
  prefix = 'NOMADA';           // es canvia DESPRÉS de crear la funció
  return etiquetar;
}

console.log(crearEtiquetador()(7));   // NOMADA-7  ← veu el valor actual, no el d'aleshores

2. Compartir estat sense adonar-se'n. Dues funcions creades a la mateixa crida comparteixen entorn:

function crearComptadorDoble() {
  let n = 0;
  return { pujar: () => ++n, llegir: () => n };
}

const c = crearComptadorDoble();
c.pujar(); c.pujar();
console.log(c.llegir());   // 2  ← comparteixen la mateixa `n`, que és el que volem aquí

És desitjable quan el busques i un error quan no.

3. Fer servir var en un bucle que crea funcions. El cas de l'apartat 9. Amb let desapareix.

4. Ombrejar una constant global amb un paràmetre. Dóna resultats false o undefined sense cap error.

5. Memoïtzar una funció impura. Retornarà dades caducades. Abans de memoïtzar, comprova que la funció és pura.

6. Pensar que l'àmbit depèn de qui crida. No: depèn d'on està escrita la funció (àmbit lèxic). Aquest és el punt que més costa i el que fa predictibles els closures.

const etiqueta = 'global';

function mostrar() {
  console.log(etiqueta);       // sempre 'global'
}

function cridar() {
  const etiqueta = 'local';    // NO afecta mostrar()
  mostrar();
}

cridar();   // global

7. Consell: si una funció necessita recordar alguna cosa entre crides, pensa en un closure abans que en una variable global. És la diferència entre estat controlat i estat solt.

8. Consell: anomena les fàbriques amb el prefix crear…. crearGeneradorIds, crearFiltrePerResponsable, crearMagatzemTasques. El nom avisa que el que retorna no és una dada, sinó una eina.

Exercicis

Exercici 1 — Predir la sortida

Sense executar el codi, escriu què imprimeix cada console.log i explica per què.

const etiqueta = 'A';

function externa() {
  const etiqueta = 'B';

  function interna() {
    console.log(etiqueta);       // (1)
  }

  if (true) {
    const etiqueta = 'C';
    console.log(etiqueta);       // (2)
    interna();                   // (3)
  }

  interna();                     // (4)
}

externa();
console.log(etiqueta);           // (5)

function comptadorTrencat() {
  var n = 0;
  const fns = [];
  for (var i = 0; i < 3; i++) {
    fns.push(() => i + n);
  }
  return fns;
}
const fns = comptadorTrencat();
console.log(fns[0](), fns[1](), fns[2]());   // (6)

Exercici 2 — crearComptadorEstat

Escriu una fàbrica crearComptadorEstat(estats) que rebi l'array d'estats del backlog i retorni un objecte amb tres funcions: registrar(estat) (suma un al recompte d'aquest estat), recompte(estat) (retorna quantes vegades s'ha registrat) i resum() (retorna una cadena amb els tres estats i els seus recomptes). El recompte ha de ser privat. Inicialitza'l recorrent l'array rebut.

Exercici 3 — Memoïtzar calcularEsforc

Escriu memoitzar(funcio), una fàbrica genèrica que rebi una funció d'un paràmetre de tipus cadena o nombre i retorni una versió memoïtzada. Aplica-la a una funció calcularEsforc(prioritat) que sigui deliberadament costosa (fes-hi un bucle d'un milió de voltes a dins) i mesura amb Date.now() la diferència entre la primera i la segona crida.

Solucions

Exercici 1

Núm. Sortida Explicació
(2) C El const del bloc if ombreja el de la funció
(3) B Àmbit lèxic: interna està escrita dins d'externa, així que veu l'etiqueta d'externa. Que es cridi des del bloc és irrellevant
(4) B Mateixa raó
(1) És la línia que produeix (3) i (4); no s'executa tota sola
(5) A La global no va ser tocada mai
(6) 3 3 3 var i és única per a tota la funció; en executar les fletxes, i ja val 3 i n val 0

Ordre real d'impressió: C, B, B, A, 3 3 3.

Per arreglar (6) n'hi ha prou amb canviar var i per let i, i aleshores donaria 0 1 2.

Exercici 2

function crearComptadorEstat(estatsInicials = []) {
  // Estat privat
  const recomptes = { pendent: 0, 'en-curs': 0, feta: 0 };

  function sumar(estat) {
    if (!(estat in recomptes)) {
      throw new Error(`Estat desconegut: ${estat}`);
    }
    recomptes[estat] = recomptes[estat] + 1;
  }

  // Inicialització a partir de l'array rebut
  for (const e of estatsInicials) {
    sumar(e);
  }

  return {
    registrar: (estat) => { sumar(estat); },
    recompte: (estat) => recomptes[estat] ?? 0,
    resum: () =>
      `pendent: ${recomptes.pendent} · en curs: ${recomptes['en-curs']} · feta: ${recomptes.feta}`
  };
}

const estats = ['en-curs', 'pendent', 'pendent', 'feta', 'en-curs', 'pendent'];
const comptador = crearComptadorEstat(estats);

console.log(comptador.resum());          // pendent: 3 · en curs: 2 · feta: 1
console.log(comptador.recompte('feta')); // 1

comptador.registrar('feta');
console.log(comptador.resum());          // pendent: 3 · en curs: 2 · feta: 2

console.log(comptador.recomptes);        // undefined  ← privat
comptador.registrar('arxivada');         // ✗ Error: Estat desconegut: arxivada

Comentari: recomptes i sumar són privats; la interfície pública només permet operacions vàlides. Ningú no pot escriure comptador.recomptes.feta = 99 perquè recomptes no existeix a fora. I el throw manté l'estat sempre coherent, aplicant el principi de fallar aviat de Gestió d'Errors.

Exercici 3

function memoitzar(funcio) {
  const cache = {};

  return function (argument) {
    const clau = String(argument);
    if (clau in cache) return cache[clau];

    const resultat = funcio(argument);
    cache[clau] = resultat;
    return resultat;
  };
}

// Funció deliberadament costosa
function calcularEsforc(prioritat) {
  let acumulat = 0;
  for (let i = 0; i < 20000000; i++) {
    acumulat += i % 3;
  }
  if (prioritat === 'alta') return 3 + (acumulat % 1);
  if (prioritat === 'mitjana') return 2 + (acumulat % 1);
  return 1 + (acumulat % 1);
}

const esforcRapid = memoitzar(calcularEsforc);

let t = Date.now();
console.log(esforcRapid('alta'), `${Date.now() - t} ms`);      // 3  ~120 ms

t = Date.now();
console.log(esforcRapid('alta'), `${Date.now() - t} ms`);      // 3  0 ms

t = Date.now();
console.log(esforcRapid('mitjana'), `${Date.now() - t} ms`);   // 2  ~120 ms

Comentari: la segona crida amb 'alta' és pràcticament instantània perquè no executa el bucle. String(argument) normalitza la clau perquè 3 i '3' comparteixin entrada (una cosa que en aquest cas volem, però que en d'altres podria ser un problema: és la limitació de fer servir un objecte com a memòria cau amb claus de text). La restricció a un paràmetre no és casual: memoïtzar amb diversos exigeix serialitzar tots els arguments en una clau, una cosa que resoldràs amb JSON.stringify a JSON i Còpies d'Objectes.

Conclusió

Ja saps on viu cada variable. L'àmbit global és per a constants de configuració com AVUI i LIMIT_SETMANAL; l'àmbit de funció neix i mor amb cada crida; i l'àmbit de bloc, que només respecten let i const, manté les variables dins de les seves claus. La cadena d'àmbits busca cada nom de dins cap a fora i s'atura a la primera coincidència, cosa que explica l'ombrejat i l'error clàssic del paràmetre que tapa una constant global. I sobretot saps que l'àmbit és lèxic: depèn d'on està escrita la funció, no de qui la crida.

D'aquí en surt el concepte central de la lliçó. Un closure és una funció que conserva viu l'entorn on va néixer, i amb ell has construït quatre eines que Nómada Tasques farà servir fins al final: crearGeneradorIds(), que garanteix la R1 sense deixar cap comptador solt; crearFiltrePerResponsable(), una fàbrica que produeix filtres ja configurats i combinables; crearMagatzemTasques(), amb estat veritablement privat i una interfície pública controlada; i crearComptadorMemoitzat(), que desa a la memòria cau resultats de funcions pures. També entens per fi el bucle amb var que retorna sempre l'últim índex, i saps que un closure reté memòria, un cost que s'estudia en profunditat a Gestió de Memòria.

Però en aquesta lliçó has donat per fet una cosa que encara no s'ha explicat: que el motor "sap" quines variables existeixen a cada àmbit abans d'executar res. És el que permet cridar una declaració de funció abans de la seva línia, el que fa que var valgui undefined en lloc de donar error, i el que fa que let i const llancin aquest misteriós Cannot access before initialization que ja has vist tres vegades. Tot això és la feina de preparació que el motor fa a cada àmbit, i és el tema de Hoisting i el Context d'Execució, on a més desmuntaràs la pila de crides que a 03-01 només vas veure per sobre.

Curs de JavaScript: De Principiant a Avançat

Mòdul 1: Introducció a JavaScript

Mòdul 2: Estructures de Control

Mòdul 3: Funcions

Mòdul 4: Objectes i Arrays

Mòdul 5: Objectes i Funcions Avançades

Mòdul 6: El Model d'Objectes del Document (DOM)

Mòdul 7: APIs del Navegador i Temes Avançats

Mòdul 8: Proves i Depuració

Mòdul 9: Rendiment i Optimització

Mòdul 10: Frameworks i Llibreries de JavaScript

Mòdul 11: Projecte Final

© Copyright 2026. Tots els drets reservats