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
- Què fa
for...ofper dins - El protocol iterador:
next()i{ value, done } - Recórrer un array a mà amb el seu iterador
- El protocol iterable:
Symbol.iterator - Fer iterable el
Tauler - Els iterables integrats i qui els consumeix
- Generadors:
function*iyield - Execució diferida i seqüències infinites
yield*: delegar en un altre iterablereturnithrowen un generador- Iteradors asíncrons i
for await...of - Quan fer servir generadors i quan no
- Errors Habituals i Consells
- Exercicis
- Conclusió
- Què fa
for...of per dins
for...of per dinsQuan escrius això:
el motor no fa el que potser imaginaves —recórrer índexs de 0 a length - 1—. Fa una cosa més general:
- Demana a l'objecte el seu iterador, cridant el seu mètode
[Symbol.iterator](). - Crida
next()sobre aquell iterador. - Si el resultat té
done: false, assigna el seuvaluea la variable del bucle i executa el cos. - 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 iterableAquí 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.
- El protocol iterador:
next() i { value, done }
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:falsesi queden elements,truequan 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.
- 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 sempreI 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.
- El protocol iterable:
Symbol.iterator
Symbol.iteratorSymbol.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.
- Fer iterable el
Tauler
TaulerAmb això, la millora que anunciava la introducció. Fins ara escrivies:
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 6Aquell 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.
- 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 codiL'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
- Generadors:
function* i yield
function* i yieldEscriure 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 3Tres coses passen aquí que no passen amb una funció normal:
- Cridar el generador no executa el seu cos.
comptarFinsA(3)retorna un objecte generador sense executar ni una línia delfor. - Cada
next()executa fins alyieldsegüent i allà es pausa, conservant totes les variables locals. - L'objecte generador és alhora iterador i iterable: té
next()i també[Symbol.iterator]()retornant-se a si mateix. Per això funciona directament en unfor...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); // 1De 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í:
- 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 polivalentNomé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); // 9Aquell 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 | Sí |
Es combina amb prendre, yield*…? |
No | Sí |
| 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.
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.
yield*: delegar en un altre iterable
yield*: delegar en un altre iterableyield* (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 hCompara-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.
return i throw en un generador
return i throw en un generadorUn 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 } ← esgotatAquell 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.
- Iteradors asíncrons i
for await...of
for await...ofFalta 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 hLes 48 h canòniques, obtingudes en tres peticions que qui consumeix el bucle no veu. Tres mèrits d'aquest disseny:
- La paginació queda encapsulada. El consumidor escriu un bucle normal; el generador s'encarrega de demanar la pàgina següent quan cal.
- És diferit de debò. Si el consumidor fa
breaka 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. - 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 polivalentUn 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.
- 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,filternireduce. 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()ambyielda dins és unSyntaxError. L'asterisc pot anar enganxat afunctiono al nom: tots dos valen. - Cridar el generador i esperar que s'executi.
comptarFinsA(3)no executa res; cal consumir-lo ambnext()o ambfor...of. - Confondre
yieldambyield*. 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 segonfor...ofno recorrerà res.[Symbol.iterator]()ha de retornar un iterador nou a cada crida. - Esperar que
for...offuncioni sobre un objecte pla. No és iterable. Fes servirObject.entries(obj), que sí que ho és. - Fer servir
for await...offora d'una funcióasync(o del nivell superior d'un mòdul). És la mateixa regla queawait. - Creure que
for await...ofparal·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-14El 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
// 6Quatre 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
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
