Fas servir for...of des del Mòdul 2 sense preguntar-te com funciona. Recorre arrays, però també strings, Map i Set; es pot interrompre amb break; i funciona igual amb estructures que no tenen índexs. Res d'això és casualitat: al darrere hi ha un protocol, un contracte que qualsevol objecte pot complir per declarar-se recorrible. Entendre'l té dues recompenses immediates. La primera és que podràs escriure for (const tasca of tauler) sobre la teva pròpia classe, en lloc de for (const tasca of tauler.tasques). La segona és que t'obre la porta als generadors, unes funcions que es pausen i es reprenen, produeixen valors només quan els hi demanen i permeten seqüències infinites sense ocupar memòria. En aquesta lliçó, l'última del mòdul, veuràs tots dos protocols per dins, faràs iterable el Tauler, reescriuràs el generador d'identificadors de 03-04, recorreràs l'arbre de subtasques de 03-07 de manera diferida i llegiràs el backlog per pàgines amb iteradors asíncrons.

Contingut

  1. Què fa for...of per dins
  2. El protocol iterador: next() i { value, done }
  3. Recórrer un array a mà amb el seu iterador
  4. El protocol iterable: Symbol.iterator
  5. Fer iterable el Tauler
  6. Els iterables integrats i qui els consumeix
  7. Generadors: function* i yield
  8. Execució diferida i seqüències infinites
  9. yield*: delegar en un altre iterable
  10. return i throw en un generador
  11. Iteradors asíncrons i for await...of
  12. Quan fer servir generadors i quan no
  13. Errors Habituals i Consells
  14. Exercicis
  15. Conclusió

  1. Què fa for...of per dins

Quan escrius això:

for (const tasca of backlog) {
  console.log(tasca.titol);
}

el motor no fa el que potser imaginaves —recórrer índexs de 0 a length - 1—. Fa una cosa més general:

  1. Demana a l'objecte el seu iterador, cridant el seu mètode [Symbol.iterator]().
  2. Crida next() sobre aquell iterador.
  3. Si el resultat té done: false, assigna el seu value a la variable del bucle i executa el cos.
  4. Repeteix el pas 2 fins a obtenir done: true.
flowchart TD
    A["for (const x of colleccio)"] --> B["colleccio[Symbol.iterator]()<br/>→ retorna un iterador"]
    B --> C["iterador.next()"]
    C --> D{"done?"}
    D -->|"false"| E["x = value<br/>executar el cos"]
    E --> C
    D -->|"true"| F["fi del bucle"]

Que el bucle no sàpiga res d'índexs és justament el que li permet recórrer un Set, un Map o un string amb la mateixa sintaxi. I també explica per què for...of no funciona sobre un objecte literal: un objecte pla no té [Symbol.iterator], així que no compleix el contracte.

const tasca = { id: 1, titol: 'Redissenyar la sala polivalent' };
// for (const x of tasca) { }
// ✗ TypeError: tasca is not iterable

Aquí hi ha l'explicació d'aquella distinció de 04-01 i 04-04 entre for...in (recorre claus de qualsevol objecte) i for...of (recorre valors d'un iterable). Són dos mecanismes completament diferents, no dues variants del mateix.

  1. El protocol iterador: next() i { value, done }

Hi ha dos contractes, i convé no barrejar-los.

Protocol Què exigeix Qui el compleix
Iterable Un mètode [Symbol.iterator]() que retorna un iterador Array, String, Map, Set, el teu Tauler
Iterador Un mètode next() que retorna { value, done } L'objecte que produeix [Symbol.iterator]()

Un iterador és un objecte amb un mètode next() que, a cada crida, retorna un objecte amb dues propietats:

  • value: l'element actual.
  • done: false si queden elements, true quan s'ha acabat.

Escrivim-ne un a mà, sense cap ajuda del llenguatge:

'use strict';

/** Iterador manual sobre les tasques obertes d'un array. */
function crearIteradorObertes(tasques) {
  let index = 0;

  return {
    next() {
      // avançar fins a la següent oberta
      while (index < tasques.length && tasques[index].estat === 'feta') {
        index++;
      }
      if (index >= tasques.length) {
        return { value: undefined, done: true };
      }
      const tasca = tasques[index];
      index++;
      return { value: tasca, done: false };
    }
  };
}

const it = crearIteradorObertes(dadesBacklog);

console.log(it.next());   // { value: { id: 1, titol: 'Redissenyar…' }, done: false }
console.log(it.next());   // { value: { id: 2, … }, done: false }

Fixa't en dues coses. Aquell index és una variable capturada per un closure (03-04): l'iterador recorda on s'havia quedat entre crides, sense exposar aquella variable a ningú. I l'iterador és d'un sol ús: quan arriba a done: true, s'ha esgotat, i cal crear-ne un altre per tornar a recórrer.

  1. Recórrer un array a mà amb el seu iterador

Els arrays ja porten el protocol implementat. Es pot fer servir directament, saltant-se el for...of:

'use strict';

const etiquetes = ['espai', 'disseny', 'fusteria'];
const it = etiquetes[Symbol.iterator]();

console.log(it.next());   // { value: 'espai',    done: false }
console.log(it.next());   // { value: 'disseny',  done: false }
console.log(it.next());   // { value: 'fusteria', done: false }
console.log(it.next());   // { value: undefined,  done: true  }
console.log(it.next());   // { value: undefined,  done: true  }  ← esgotat per sempre

I aquest és literalment el for...of desmuntat, escrit amb un while:

const iterador = etiquetes[Symbol.iterator]();
let resultat = iterador.next();

while (!resultat.done) {
  const etiqueta = resultat.value;
  console.log(etiqueta);
  resultat = iterador.next();
}

Ningú no escriu així en el dia a dia —per a això hi ha for...of—, però veure-ho una vegada fixa el model mental. I explica de passada un detall important: com que el bucle només demana next() quan necessita el valor següent, un break simplement deixa de demanar. D'aquí que for...of es pugui interrompre i forEach no.

  1. El protocol iterable: Symbol.iterator

Symbol.iterator és un símbol ben conegut: un valor únic que el llenguatge fa servir com a nom de propietat per a aquest contracte. S'escriu entre claudàtors perquè és una clau computada, la sintaxi que vas aprendre a 04-01.

'use strict';

const rangDeIds = {
  des: 1,
  fins: 6,

  [Symbol.iterator]() {
    let actual = this.des;
    const fi = this.fins;

    return {
      next() {
        if (actual > fi) return { value: undefined, done: true };
        return { value: actual++, done: false };
      }
    };
  }
};

for (const id of rangDeIds) console.log(id);       // 1 2 3 4 5 6
console.log([...rangDeIds]);                        // [ 1, 2, 3, 4, 5, 6 ]

Amb un sol mètode aquell objecte passa a funcionar amb for...of, amb el spread de 04-07, amb la desestructuració de 04-06 i amb Array.from. Aquesta és la potència de programar contra un protocol: implementes el contracte una vegada i guanyes tota la sintaxi que el consumeix.

Per què un símbol i no simplement el nom 'iterator'? Per evitar col·lisions. Un objecte pot tenir una propietat iterator amb qualsevol significat sense interferir amb el protocol, perquè Symbol.iterator és un valor únic i irrepetible que ningú no pot reproduir per accident.

  1. Fer iterable el Tauler

Amb això, la millora que anunciava la introducció. Fins ara escrivies:

for (const tasca of tauler.tasques) { … }        // cal conèixer la propietat interna

Afegint el protocol, el tauler es recorre a si mateix:

// js/model/tauler.js
export class Tauler {
  #tasques = [];

  // …tot el del 05-04…

  /** Fa que el tauler sigui iterable: for (const tasca of tauler). */
  [Symbol.iterator]() {
    return this.#tasques[Symbol.iterator]();      // deleguem en l'array intern
  }
}

Una sola línia, perquè l'array ja sap iterar-se. I tot això comença a funcionar:

const tauler = new Tauler('Taller Nómada', crearBacklog());

for (const tasca of tauler) {
  console.log(tasca.descripcio(AVUI));
}

const totes = [...tauler];                            // spread
const [primera, segona] = tauler;                     // desestructuració
const titols = Array.from(tauler, (t) => t.titol);    // Array.from amb transformació

console.log(totes.length);      // 6
console.log(primera.titol);     // 'Redissenyar la sala polivalent'
console.log(titols.at(-1));     // 'Pressupost de la fusteria'

Fixa't en el que això significa per a l'encapsulació de 05-03: el consumidor recorre les tasques sense que li exposem l'array intern. De fet, #tasques continua sent privat, i el getter tasques que retornava una còpia ja ni tan sols fa falta per recórrer.

Es poden oferir a més diverses formes de recorregut com a mètodes que retornen iterables:

export class Tauler {
  // …

  /** Recorregut per defecte: totes les tasques, en ordre d'inserció. */
  [Symbol.iterator]() {
    return this.#tasques[Symbol.iterator]();
  }

  /** Només les obertes. Retorna un iterable, no un array. */
  obertesIterables() {
    const tasques = this.#tasques;
    return {
      [Symbol.iterator]() {
        let i = 0;
        return {
          next() {
            while (i < tasques.length && !tasques[i].oberta) i++;
            if (i >= tasques.length) return { done: true, value: undefined };
            return { done: false, value: tasques[i++] };
          }
        };
      }
    };
  }
}

for (const tasca of tauler.obertesIterables()) console.log(tasca.id);   // 1 2 3 5 6

Aquell obertesIterables funciona, però mira quanta cerimònia: un objecte amb [Symbol.iterator] que retorna un altre objecte amb next que gestiona un índex a mà. És incòmode d'escriure i fàcil d'equivocar. A l'apartat 7 el reescriuràs en tres línies amb un generador.

  1. Els iterables integrats i qui els consumeix

Aquests són els iterables que porta el llenguatge o el navegador:

Iterable Què produeix en recórrer-lo
Array Els seus elements, en ordre
String Els seus caràcters, respectant els emojis i caràcters compostos
Map Parells [clau, valor]
Set Els seus valors, sense duplicats
arguments Els arguments d'una funció (objecte tipus array, no un array)
NodeList (Mòdul 6) Els nodes del DOM retornats per querySelectorAll
TypedArray Els seus nombres
El resultat d'entries(), keys(), values() Els seus elements

Un detall que sorprèn amb els strings:

const text = 'ràfia';
console.log([...text]);           // [ 'r', 'à', 'f', 'i', 'a' ]
console.log('👩‍🎨'.length);          // 5  ← unitats de codi
console.log([...'👩‍🎨'].length);     // 3  ← punts de codi

L'iterador de String recorre punts de codi, no unitats de 16 bits, així que tracta millor els caràcters no llatins que l'índex numèric. És una raó pràctica per preferir for...of a un for clàssic en recórrer text.

I aquestes són les operacions que consumeixen un iterable —totes funcionen amb el Tauler des que li vam posar el protocol—:

Operació Exemple
for...of for (const t of tauler) …
Spread en array [...tauler]
Spread en arguments Math.max(...hores)
Desestructuració d'arrays const [primera] = tauler;
Array.from Array.from(tauler, (t) => t.titol)
Constructors de Set i Map new Set(tauler)
Promise.all i companyia Promise.all(promeses)
yield* apartat 9

Un avís important: Object.keys, for...in, JSON.stringify i map/filter/reduce no fan servir aquest protocol. Els mètodes d'array són mètodes d'Array.prototype (05-01) i només existeixen en arrays. Per això, per aplicar filter a un iterable qualsevol, primer cal materialitzar-lo:

const obertes = [...tauler].filter((t) => t.oberta);      // ✓
// tauler.filter(...)  → només si la classe defineix aquell mètode

  1. Generadors: function* i yield

Escriure iteradors a mà és tediós. Els generadors són funcions que els construeixen automàticament.

Es declaren amb function* i produeixen valors amb yield:

'use strict';

function* comptarFinsA(n) {
  for (let i = 1; i <= n; i++) {
    yield i;               // "lliura aquest valor i pausa aquí"
  }
}

const gen = comptarFinsA(3);

console.log(gen.next());   // { value: 1, done: false }
console.log(gen.next());   // { value: 2, done: false }
console.log(gen.next());   // { value: 3, done: false }
console.log(gen.next());   // { value: undefined, done: true }

for (const n of comptarFinsA(3)) console.log(n);   // 1 2 3

Tres coses passen aquí que no passen amb una funció normal:

  1. Cridar el generador no executa el seu cos. comptarFinsA(3) retorna un objecte generador sense executar ni una línia del for.
  2. Cada next() executa fins al yield següent i allà es pausa, conservant totes les variables locals.
  3. L'objecte generador és alhora iterador i iterable: té next() i també [Symbol.iterator]() retornant-se a si mateix. Per això funciona directament en un for...of.

Aquella pausa és la característica única: una funció normal, un cop comença, corre fins al return. Un generador es pot aturar a mitges i continuar després, exactament on ho va deixar.

Ara reescriu l'obertesIterables de l'apartat 5:

export class Tauler {
  // …

  /** Només les tasques obertes, de manera diferida. */
  *obertes() {
    for (const tasca of this.#tasques) {
      if (tasca.oberta) yield tasca;
    }
  }

  /** Tasques d'un responsable. */
  *de(responsable) {
    for (const tasca of this.#tasques) {
      if (tasca.responsable === responsable) yield tasca;
    }
  }

  /** Tasques vençudes a una data donada (R10). */
  *vencudes(avui) {
    for (const tasca of this.#tasques) {
      if (tasca.estaVencuda(avui)) yield tasca;
    }
  }
}

for (const t of tauler.obertes()) console.log(t.id);         // 1 2 3 5 6
for (const t of tauler.de('Iván')) console.log(t.titol);     // les 3 de l'Iván
console.log([...tauler.vencudes(AVUI)].length);              // 1

De vint línies de cerimònia a tres. Fixa't en la sintaxi *obertes(): és un mètode generador dins d'una classe, i també existeix en objectes literals (*metode() { … }) i com el mateix *[Symbol.iterator]().

De fet, l'iterable del tauler es pot escriure així:

*[Symbol.iterator]() {
  yield* this.#tasques;        // el yield* de l'apartat 9
}

  1. Execució diferida i seqüències infinites

Que un generador només treballi quan se li demana s'anomena avaluació diferida (lazy), i té dues conseqüències enormes.

Conseqüència 1: no es calcula el que no es fa servir.

'use strict';

function* tasquesAmbTraca(tasques) {
  for (const t of tasques) {
    console.log(`  · avaluant ${t.id}`);
    yield t;
  }
}

// Cerquem la primera de més de 10 h
for (const t of tasquesAmbTraca(dadesBacklog)) {
  if (t.horesEstimades > 10) {
    console.log(`Trobada: ${t.titol}`);
    break;                                   // ← el generador no continua
  }
}
//   · avaluant 1
// Trobada: Redissenyar la sala polivalent

Només es va avaluar una tasca. Compara-ho amb dadesBacklog.filter(t => t.horesEstimades > 10)[0], que recorre les sis i construeix un array intermedi. Amb sis elements és igual; amb dues-centes mil, la diferència és abismal.

Conseqüència 2: es poden representar seqüències infinites. Un array infinit és impossible; un generador infinit és trivial, perquè els valors no existeixen fins que es demanen.

Reprenem el generador d'identificadors de 03-04, aquell closure que retornava una funció:

// ── Versió de 03-04, amb closure ──────────────────────────────────
function crearGeneradorIds(inicial = 0) {
  let ultimId = inicial;
  return function seguentId() {
    ultimId = ultimId + 1;
    return ultimId;
  };
}

// ── Versió amb generador ──────────────────────────────────────────
function* generadorIds(inicial = 0) {
  let id = inicial;
  while (true) {              // ✓ bucle infinit perfectament segur
    id += 1;
    yield id;
  }
}

const ids = generadorIds(6);        // el backlog canònic arriba fins al 6

console.log(ids.next().value);      // 7
console.log(ids.next().value);      // 8
console.log(ids.next().value);      // 9

Aquell while (true) no penja res perquè el generador està aturat al yield la major part del temps: només avança una volta per cada next(). Compara els dos enfocaments:

Closure (03-04) Generador
Com es demana el següent seguentId() ids.next().value
Estat intern Variable capturada Variables locals pausades
Funciona amb for...of? No
Es combina amb prendre, yield*…? No
Llegibilitat de la seqüència S'infereix del cos Explícita: es llegeix com un bucle

Compte amb un parany evident: mai no facis spread d'un generador infinit.

// const tots = [...generadorIds()];      // ⚠ penja el programa per sempre

Per consumir-ne una part, cal una funció que prengui els primers N —i que, com qualsevol altra combinació d'iterables, també és un generador:

/** Pren els n primers elements de qualsevol iterable. */
function* prendre(iterable, n) {
  let comptats = 0;
  for (const valor of iterable) {
    if (comptats >= n) return;
    yield valor;
    comptats++;
  }
}

console.log([...prendre(generadorIds(6), 5)]);   // [ 7, 8, 9, 10, 11 ]

I ja que hi som, map i filter diferits, que funcionen sobre seqüències infinites al contrari que els seus homònims d'array:

function* mapejar(iterable, transformar) {
  for (const v of iterable) yield transformar(v);
}

function* filtrar(iterable, predicat) {
  for (const v of iterable) if (predicat(v)) yield v;
}

const idsParells = filtrar(generadorIds(0), (n) => n % 2 === 0);
const etiquetats = mapejar(idsParells, (n) => `T-${String(n).padStart(3, '0')}`);

console.log([...prendre(etiquetats, 4)]);   // [ 'T-002', 'T-004', 'T-006', 'T-008' ]

Aquella cadena processa els valors d'un en un a través de les tres funcions, sense construir ni un sol array intermedi. És la composició de 03-06 aplicada a fluxos de dades.

  1. yield*: delegar en un altre iterable

yield* (amb asterisc) lliura tots els valors d'un altre iterable, un a un, com si estiguessin escrits allà.

function* primers() { yield 1; yield 2; }
function* segons() { yield 3; yield 4; }

function* tots() {
  yield* primers();
  yield* segons();
  yield 5;
}

console.log([...tots()]);         // [ 1, 2, 3, 4, 5 ]

Sense l'asterisc, yield primers() lliuraria l'objecte generador com un únic valor, no els seus elements.

La seva aplicació estrella és la recursió sobre estructures imbricades, i aquí reprenem l'arbre de subtasques de 03-07: «Redissenyar la sala polivalent» amb les seves 12 h repartides i 7 h obertes.

'use strict';

const tascaRedisseny = {
  id: 1, titol: 'Redissenyar la sala polivalent', responsable: 'Iván',
  estat: 'en-curs', horesEstimades: 0,
  subtasques: [
    { id: 11, titol: 'Mesurar i aixecar el plànol', responsable: 'Iván',
      estat: 'feta', horesEstimades: 3, subtasques: [] },
    { id: 12, titol: 'Triar el mobiliari', responsable: 'Marta',
      estat: 'en-curs', horesEstimades: 0,
      subtasques: [
        { id: 121, titol: 'Demanar pressupostos', responsable: 'Marta',
          estat: 'feta', horesEstimades: 2, subtasques: [] },
        { id: 122, titol: 'Visitar dos proveïdors', responsable: 'Marta',
          estat: 'pendent', horesEstimades: 2, subtasques: [] }
      ] },
    { id: 13, titol: 'Pintar i muntar', responsable: 'Iván',
      estat: 'pendent', horesEstimades: 5, subtasques: [] }
  ]
};

/** Recorre un arbre de tasques en profunditat, de manera diferida. */
function* recorrerArbre(tasca, profunditat = 0) {
  yield { tasca, profunditat };
  for (const sub of tasca.subtasques ?? []) {
    yield* recorrerArbre(sub, profunditat + 1);      // ← delegació recursiva
  }
}

for (const { tasca, profunditat } of recorrerArbre(tascaRedisseny)) {
  console.log(`${'  '.repeat(profunditat)}[${tasca.id}] ${tasca.titol} · ${tasca.horesEstimades} h`);
}
// [1] Redissenyar la sala polivalent · 0 h
//   [11] Mesurar i aixecar el plànol · 3 h
//   [12] Triar el mobiliari · 0 h
//     [121] Demanar pressupostos · 2 h
//     [122] Visitar dos proveïdors · 2 h
//   [13] Pintar i muntar · 5 h

Compara-ho amb l'aplanarTasques de 03-07, que construïa un array complet amb concat. Aquí no es crea cap array: els nodes es van lliurant a mesura que es visiten. I sobre aquell recorregut diferit tota la resta surt de franc:

const nodes = [...recorrerArbre(tascaRedisseny)].map((n) => n.tasca);

console.log(nodes.reduce((s, t) => s + t.horesEstimades, 0));                       // 12
console.log(nodes.filter((t) => t.estat !== 'feta').reduce((s, t) => s + t.horesEstimades, 0));   // 7

// Cercar sense recórrer l'arbre sencer: s'atura tan bon punt troba
function cercarPerId(arrel, id) {
  for (const { tasca } of recorrerArbre(arrel)) {
    if (tasca.id === id) return tasca;
  }
  return null;
}
console.log(cercarPerId(tascaRedisseny, 121).titol);   // 'Demanar pressupostos'

Les 12 h totals i 7 h obertes de l'arbre canònic, amb un recorregut que a més es pot interrompre a mitges. Aquest és l'avantatge concret de l'avaluació diferida sobre la recursió que retorna arrays.

  1. return i throw en un generador

Un objecte generador té dos mètodes més, a part de next().

return(valor) acaba el generador anticipadament:

function* comptar() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log('neteja del generador');       // s'executa igualment
  }
}

const g = comptar();
console.log(g.next());        // { value: 1, done: false }
console.log(g.return(99));    // neteja del generador
                              // { value: 99, done: true }
console.log(g.next());        // { value: undefined, done: true }  ← esgotat

Aquell finally és la raó principal que aquest mètode existeixi: permet alliberar recursos —tancar un fitxer, una connexió— encara que el consumidor abandoni a mitges. I el més important: for...of crida return() automàticament quan surts amb break, return o una excepció. És a dir, la neteja està garantida:

for (const n of comptar()) {
  if (n === 2) break;         // provoca la crida a return() → s'imprimeix la neteja
}

throw(error) injecta una excepció al punt on el generador està pausat:

function* processar() {
  try {
    yield 'llest';
    yield 'mai no hi arribo';
  } catch (error) {
    console.log('el generador ha capturat:', error.message);
    yield 'recuperat';
  }
}

const p = processar();
console.log(p.next().value);                           // 'llest'
console.log(p.throw(new Error('error extern')).value); // el generador ha capturat: error extern
                                                       // 'recuperat'

Es fa servir poc en codi d'aplicació, però convé conèixer-lo: juntament amb la possibilitat de passar un valor a next(v) —que arriba com a resultat del yield on el generador estava pausat—, forma un canal de comunicació bidireccional. Sobre aquell mecanisme es van construir les primeres implementacions d'async/await abans que fos sintaxi del llenguatge.

  1. Iteradors asíncrons i for await...of

Falta l'últim cas: recórrer alguna cosa els elements de la qual arriben amb el temps, com les pàgines de resultats d'un servidor. Per a això existeixen els protocols asíncrons, que són la versió amb promeses dels dos anteriors.

Síncron Asíncron
Mètode de l'iterable [Symbol.iterator]() [Symbol.asyncIterator]()
Què retorna next() { value, done } Una promesa de { value, done }
Com es declara el generador function* async function*
Com es consumeix for...of for await...of

Aplicat a llegir el backlog de Taller Nómada per pàgines simulades:

// js/dades/paginat.js
import { Tasca } from '../model/tasca.js';
import { dadesBacklog } from './backlog.js';

const esperar = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

/**
 * Simula una API paginada. Al Mòdul 7 això serà fetch amb ?pagina=N (07-02).
 * @returns {Promise<{tasques: Object[], hiHaMes: boolean}>}
 */
async function llegirPagina(numero, perPagina = 2) {
  await esperar(300);                                   // latència simulada
  const inici = (numero - 1) * perPagina;
  const tros = dadesBacklog.slice(inici, inici + perPagina);
  return { tasques: tros, hiHaMes: inici + perPagina < dadesBacklog.length };
}

/** Generador asíncron: lliura les tasques pàgina a pàgina, sota demanda. */
export async function* llegirBacklogPaginat(perPagina = 2) {
  let pagina = 1;
  let hiHaMes = true;

  while (hiHaMes) {
    console.log(`  ⇩ demanant la pàgina ${pagina}…`);
    const resposta = await llegirPagina(pagina, perPagina);
    for (const dades of resposta.tasques) {
      yield new Tasca(dades);                           // lliura tasca a tasca
    }
    hiHaMes = resposta.hiHaMes;
    pagina += 1;
  }
}

I el seu consum, que amaga completament la paginació:

import { llegirBacklogPaginat } from './dades/paginat.js';

let hores = 0;
for await (const tasca of llegirBacklogPaginat()) {
  console.log(tasca.descripcio(AVUI));
  hores += tasca.horesEstimades;
}
console.log(`Total: ${hores} h`);

//   ⇩ demanant la pàgina 1…
// ▸ [1] Redissenyar la sala polivalent · Iván · 12 h
// ○ [2] Cartelleria del taller de serigrafia · Marta · 6 h
//   ⇩ demanant la pàgina 2…
// ○ [3] Actualitzar el web de reserves · Lucía · 14 h
// ✓ [4] Inventari de tintes de serigrafia · Marta · 3 h
//   ⇩ demanant la pàgina 3…
// ▸ [5] Guia sobre enquadernació per a residents · Iván · 8 h
// ○ [6] Pressupost de la fusteria · Iván · 5 h ⚠ VENÇUDA
// Total: 48 h

Les 48 h canòniques, obtingudes en tres peticions que qui consumeix el bucle no veu. Tres mèrits d'aquest disseny:

  1. La paginació queda encapsulada. El consumidor escriu un bucle normal; el generador s'encarrega de demanar la pàgina següent quan cal.
  2. És diferit de debò. Si el consumidor fa break a la tercera tasca, les pàgines 2 i 3 no es demanen mai. Amb una funció que retornés tot l'array, s'haurien demanat igualment.
  3. La memòria és constant. Només hi ha una pàgina a la memòria alhora, encara que el backlog tingués un milió de tasques.
// Prova de l'avaluació diferida: només es demana la primera pàgina
for await (const tasca of llegirBacklogPaginat()) {
  if (tasca.horesEstimades > 10) { console.log('Trobada:', tasca.titol); break; }
}
//   ⇩ demanant la pàgina 1…
// Trobada: Redissenyar la sala polivalent

Un apunt de 05-06: for await...of processa els elements seqüencialment, esperant cadascun. Si el que vols és llançar-ho tot en paral·lel, continua sent Promise.all. Aquí la seqüencialitat és desitjada: no es pot demanar la pàgina 2 sense saber si existeix.

  1. Quan fer servir generadors i quan no

Els generadors són elegants i per això se n'abusa. La regla honesta:

Fes servir un generador quan… Fes servir un array o una funció normal quan…
La seqüència és infinita o de longitud desconeguda Saps quants elements hi ha i caben a la memòria
Produir cada element és car i potser no els necessites tots Produir-los és barat
Vols poder interrompre el recorregut a mitges El recorreràs sencer sempre
Recorres una estructura recursiva (arbres) i vols avaluació diferida Un map/filter normal resol el cas
Les dades arriben per trossos (pàgines, fluxos) Les tens totes de cop
Vols encadenar transformacions sense arrays intermedis La llegibilitat de filter().map() és preferible

I els inconvenients reals, que cal sospesar:

  • No hi ha length. No pots saber quants elements produirà sense consumir-lo sencer.
  • Un sol ús. Consumit un generador, s'ha acabat; cal crear-ne un altre. Un array es recorre les vegades que vulguis.
  • No té map, filter ni reduce. Cal escriure'ls (com a l'apartat 8) o materialitzar amb [...gen], cosa que anul·la l'avaluació diferida.
  • Depurar és més difícil. Posar un punt d'interrupció dins d'un generador que es pausa i es reprèn desconcerta al principi.
  • Amb pocs elements, no compensa. Per al backlog de sis tasques, filter és més clar i no hi ha cap guany. L'avaluació diferida comença a pagar amb milers d'elements, amb producció cara o amb dades que arriben de fora.

A Nómada Tasques els fem servir on de debò aporten: [Symbol.iterator] al Tauler per comoditat de l'API, el recorregut diferit de l'arbre de subtasques, i el generador asíncron del backlog paginat.

Errors Habituals i Consells

  • Oblidar l'asterisc. function generador() amb yield a dins és un SyntaxError. L'asterisc pot anar enganxat a function o al nom: tots dos valen.
  • Cridar el generador i esperar que s'executi. comptarFinsA(3) no executa res; cal consumir-lo amb next() o amb for...of.
  • Confondre yield amb yield*. Sense asterisc lliures l'iterable sencer com un valor; amb asterisc en lliures els elements.
  • Fer spread d'un generador infinit. [...generadorIds()] penja el programa. Fes servir una funció prendre(n).
  • Reutilitzar un generador esgotat. Retorna { done: true } per sempre i el bucle no dona ni una volta. Si necessites recórrer dues vegades, crea dos generadors o materialitza en un array.
  • Retornar el mateix iterador des de [Symbol.iterator](). Si guardes l'iterador en una propietat i el retornes sempre, el segon for...of no recorrerà res. [Symbol.iterator]() ha de retornar un iterador nou a cada crida.
  • Esperar que for...of funcioni sobre un objecte pla. No és iterable. Fes servir Object.entries(obj), que sí que ho és.
  • Fer servir for await...of fora d'una funció async (o del nivell superior d'un mòdul). És la mateixa regla que await.
  • Creure que for await...of paral·lelitza. És seqüencial per definició. Per a paral·lel, Promise.all (05-06).
  • Consell: quan dubtis de si alguna cosa és iterable, pregunta-l'hi: typeof x?.[Symbol.iterator] === 'function'. És més fiable que suposar-ho.

Exercicis

Exercici 1 — Iterable a mà. Escriu una classe Setmana que rebi una data ISO d'inici i sigui iterable, produint els set dies d'aquella setmana en format 'yyyy-mm-dd'. Implementa-la sense generadors, amb [Symbol.iterator]() retornant un objecte amb next(). Comprova-la amb for...of, amb spread i amb desestructuració, i verifica que es pot recórrer dues vegades seguides.

Exercici 2 — Generadors compostos. Escriu tres generadors —prendre(iterable, n), saltar(iterable, n) i perLots(iterable, mida)— i fes-los servir per produir, a partir d'un generadorIds(6) infinit, els identificadors de l'11 al 20 agrupats en lots de 3. Després aplica'ls al backlog per obtenir les tasques obertes en lots de 2.

Exercici 3 — Recorregut asíncron amb tall. Amplia llegirBacklogPaginat perquè accepti un objecte d'opcions { perPagina, filtre } i només lliuri les tasques que compleixin el filtre. Escriu després primeraQueCompleix(iterableAsincron, predicat) que retorni la primera coincidència aturant el recorregut, i demostra amb la traça de consola que només es va demanar la pàgina necessària.

Solucions

Exercici 1

'use strict';

class Setmana {
  #inici;

  constructor(dataISO) {
    this.#inici = dataISO;
  }

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

  [Symbol.iterator]() {
    const base = new Date(this.#inici);
    let dia = 0;                                  // ← estat NOU a cada crida

    return {
      next() {
        if (dia >= 7) return { value: undefined, done: true };
        const data = new Date(base);
        data.setDate(base.getDate() + dia);
        dia += 1;
        return { value: data.toISOString().slice(0, 10), done: false };
      }
    };
  }
}

const setmana = new Setmana('2026-09-14');        // la setmana de l'AVUI canònic

for (const dia of setmana) console.log(dia);
// 2026-09-14 … 2026-09-20

console.log([...setmana].length);                 // 7
const [dilluns, dimarts] = setmana;
console.log(dilluns, dimarts);                    // 2026-09-14 2026-09-15

// Es recorre dues vegades sense problema:
console.log([...setmana][6], [...setmana][0]);    // 2026-09-20 2026-09-14

El punt crític de l'exercici és a les dues línies que declaren base i dia dins de [Symbol.iterator](). Si les haguessis posades com a camps de la classe, el primer recorregut deixaria dia a 7 i el segon for...of no donaria ni una volta: és l'error de «retornar el mateix iterador» dels errors habituals. Cada crida al mètode ha de produir un iterador verge, amb el seu propi estat capturat per closure.

Com a comparació, la versió amb generador cap en quatre línies i no té aquell risc:

*[Symbol.iterator]() {
  const base = new Date(this.#inici);
  for (let d = 0; d < 7; d++) {
    const data = new Date(base);
    data.setDate(base.getDate() + d);
    yield data.toISOString().slice(0, 10);
  }
}

Exercici 2

'use strict';

function* prendre(iterable, n) {
  if (n <= 0) return;
  let comptats = 0;
  for (const v of iterable) {
    yield v;
    comptats++;
    if (comptats >= n) return;
  }
}

function* saltar(iterable, n) {
  let saltats = 0;
  for (const v of iterable) {
    if (saltats < n) { saltats++; continue; }
    yield v;
  }
}

function* perLots(iterable, mida) {
  let lot = [];
  for (const v of iterable) {
    lot.push(v);
    if (lot.length === mida) {
      yield lot;
      lot = [];                     // array nou: no reutilitzar el lliurat
    }
  }
  if (lot.length > 0) yield lot;    // l'últim lot incomplet
}

// Ids de l'11 al 20, en lots de 3
const ids = generadorIds(6);                     // produeix 7, 8, 9, …
const delOnzeAlVint = prendre(saltar(ids, 4), 10);   // salta 7-10, pren 11-20

console.log([...perLots(delOnzeAlVint, 3)]);
// [ [ 11, 12, 13 ], [ 14, 15, 16 ], [ 17, 18, 19 ], [ 20 ] ]

// Tasques obertes del backlog, en lots de 2
const tauler = new Tauler('Taller Nómada', crearBacklog());
for (const lot of perLots(tauler.obertes(), 2)) {
  console.log(lot.map((t) => t.id).join(', '));
}
// 1, 2
// 3, 5
// 6

Quatre observacions. La composició prendre(saltar(ids, 4), 10) funciona perquè tot generador és iterable, així que un pot consumir-ne un altre: és exactament la idea de compondre/pipe de 03-06, ara sobre fluxos. Res no es materialitza fins al [...] final, i el generador infinit no dona mai problemes perquè prendre deixa de demanar-li valors. El lot = [] després de cada yield és imprescindible: si reutilitzessis el mateix array, tots els lots lliurats serien el mateix objecte i acabarien amb el contingut de l'últim —el mateix problema de referències compartides de 04-08—. I l'últim if fora del bucle lliura el lot incomplet, que és gairebé sempre el que es vol.

Exercici 3

'use strict';

/**
 * Llegeix el backlog paginat, lliurant només les tasques que passen el filtre.
 * @param {Object}   [opcions]
 * @param {number}   [opcions.perPagina=2]
 * @param {Function} [opcions.filtre]  (tasca) => boolean
 */
export async function* llegirBacklogPaginat({ perPagina = 2, filtre = () => true } = {}) {
  let pagina = 1;
  let hiHaMes = true;

  while (hiHaMes) {
    console.log(`  ⇩ demanant la pàgina ${pagina}…`);
    const resposta = await llegirPagina(pagina, perPagina);
    for (const dades of resposta.tasques) {
      const tasca = new Tasca(dades);
      if (filtre(tasca)) yield tasca;
    }
    hiHaMes = resposta.hiHaMes;
    pagina += 1;
  }
}

/** Primera coincidència d'un iterable asíncron; atura el recorregut. */
async function primeraQueCompleix(iterableAsincron, predicat) {
  for await (const element of iterableAsincron) {
    if (predicat(element)) return element;
  }
  return null;
}

// Cas 1 · la coincidència és a la primera pàgina
const primera = await primeraQueCompleix(
  llegirBacklogPaginat({ filtre: (t) => t.oberta }),
  (t) => t.horesEstimades > 10
);
console.log('→', primera.titol);
//   ⇩ demanant la pàgina 1…
// → Redissenyar la sala polivalent          ← una sola petició

// Cas 2 · la coincidència és a l'última pàgina
const vencuda = await primeraQueCompleix(
  llegirBacklogPaginat({ filtre: (t) => t.oberta }),
  (t) => t.estaVencuda(AVUI)
);
console.log('→', vencuda.titol);
//   ⇩ demanant la pàgina 1…
//   ⇩ demanant la pàgina 2…
//   ⇩ demanant la pàgina 3…
// → Pressupost de la fusteria               ← tres peticions, les justes

// Cas 3 · sense coincidències: s'esgota l'iterable
console.log(await primeraQueCompleix(llegirBacklogPaginat(), (t) => t.horesEstimades > 100));
// null (després de les tres pàgines)

La traça és la demostració de l'exercici: al primer cas es demana una pàgina, al segon tres, i en cap moment no es demanen pàgines de més. Ho aconsegueix el return dins del for await...of, que provoca que el bucle cridi el return() de l'iterador asíncron —el mateix mecanisme de l'apartat 10— i el generador s'aturi netament al seu yield, sense arribar mai a la iteració següent del while.

Compara això amb l'alternativa ingènua: una funció que descarregui totes les pàgines, construeixi un array de les 6 tasques i després faci find. Amb sis tasques és idèntic; amb un backlog de deu mil repartit en cinc mil pàgines, la diferència entre una petició i cinc mil és la diferència entre una aplicació usable i una d'inservible. Aquest és exactament el patró que aplicaràs al Mòdul 7 en paginar dades reals amb fetch.

Conclusió

Has arribat al mecanisme que explica una sintaxi que feies servir des del Mòdul 2. for...of no recorre índexs: demana un iterador amb [Symbol.iterator]() i el consulta amb next() fins a rebre { done: true }. Són dos contractes diferents —iterable, el que sap produir un iterador; iterador, el que sap lliurar valors d'un en un— i n'hi ha prou d'implementar el primer per guanyar de cop for...of, el spread, la desestructuració, Array.from i els constructors de Set i Map. Ho has comprovat escrivint un iterador a mà, desmuntant el for...of en un while, i afegint una línia a Tauler per poder escriure for (const tasca of tauler) sense exposar l'array privat. També saps per què for...of s'interromp amb break i forEach no, per què un objecte pla no és iterable, i per què map, filter i Object.keys juguen en una altra lliga: són mètodes d'array, no consumidors del protocol.

Sobre aquesta base has descobert els generadors: funcions function* que es pausen a cada yield i es reprenen a cada next(), conservant les seves variables locals. Cridar-les no executa res; l'objecte generador que retornen és alhora iterador i iterable. Aquella avaluació diferida té dues conseqüències que has explotat a fons: no es calcula el que no es demana —la cerca que avalua una sola tasca i s'atura— i les seqüències poden ser infinites, com el generadorIds amb el seu while (true) perfectament segur que reescriu el closure de 03-04 guanyant compatibilitat amb for...of i amb la composició. Amb prendre, saltar, mapejar, filtrar i perLots has muntat canonades de dades sense ni un sol array intermedi, i amb yield* has recorregut l'arbre de subtasques de 03-07 de manera diferida i interrompible, recuperant-ne les 12 h totals i les 7 h obertes sense construir la llista aplanada. Coneixes return() i throw() sobre un generador, i el detall valuós que for...of crida return() automàticament en sortir amb break, cosa que garanteix l'execució de qualsevol finally de neteja. I has tancat el cercle amb els protocols asíncrons: async function*, Symbol.asyncIterator i for await...of aplicats a un backlog paginat que amaga la paginació al consumidor, demana només les pàgines necessàries i manté la memòria constant. Saps, finalment, quan no fer-los servir: sense length, d'un sol ús, sense mètodes d'array i sense cap avantatge quan les dades són poques i barates.

Amb això es tanca el Mòdul 5, i val la pena mirar enrere. Hi vas entrar amb sis objectes literals que repetien els seus mètodes un a un i en surts amb un model complet: els prototips que expliquen com JavaScript comparteix comportament de debò; les classes que ho escriuen de manera llegible, amb extends, super i polimorfisme; l'encapsulació amb #estat, getters, setters i una API pública dissenyada a consciència, que fa impossible deixar una tasca en un estat il·legal; els mòduls que reparteixen el projecte en model/, dades/ i util/ amb dependències explícites i sense cicles; i el salt sencer a l'asincronia —callbacks, promeses, async/await, el bucle d'esdeveniments i les microtasques, iteradors asíncrons—, amb carregarTauler() capaç de portar les dades amb latència, temps màxim, validació i reserva local. Els números canònics de Taller Nómada continuen sent els mateixos que al Mòdul 1 —48 h totals, 45 obertes, l'Iván amb 25, la Lucía amb 14, la Marta amb 6, la fusteria vençuda, esforç ponderat 124—, però la maquinària que els produeix ja no és un guió: és una aplicació.

I aquí arriba el canvi d'escenari. Tot el que has construït en cinc mòduls viu a la consola. La Marta, l'Iván i la Lucía no obriran les eines de desenvolupament per saber què han de fer avui: necessiten veure el tauler a la pantalla, marcar una tasca com a feta amb un clic, filtrar per responsable i donar d'alta una de nova des d'un formulari. El model està construït, protegit, modularitzat i provat per consola; ha arribat el moment que es vegi. Això significa aprendre a parlar amb la pàgina: seleccionar elements, crear-los, modificar-los i reaccionar al que fa l'usuari —i, amb el que ara saps del bucle d'esdeveniments, entenent exactament per què un gestor lent congela la interfície—. És el Mòdul 6: El Model d'Objectes del Document, que comença amb Introducció al DOM.

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