Gli oggetti iterabili sono una generalizzazione degli array. Questo concetto consente a qualsiasi oggetto di essere utilizzato in un ciclo for..of.
Ovviamente, gli array sono oggetti iterabili. Ma ci sono molti altri oggetti integrati, che sono altresì iterabili. Ad esempio, le stringhe.
Se un oggetto rappresenta una collezione (lista, insieme) di qualcosa, allora for..of è un ottimo modo per eseguire un ciclo, quindi ora vedremo come farlo funzionare correttamente.
Symbol.iterator
Possiamo spiegare meglio il funzionamento degli oggetti iterabili costruendone uno nostro.
Ad esempio, abbiamo un oggetto, che non è un array, ma sembra essere adatto ad un for..of.
Come un oggetto range che rappresenta un intervallo numerico:
let range = {
from: 1,
to: 5
};
// Vorremmo che il for..of funzioni:
// for(let num of range) ... num=1,2,3,4,5
Per rendere iterabile l’oggetto range (e poter quindi utilizzare correttamente for..of) abbiamo bisogno di aggiungervi un metodo chiamato Symbol.iterator (uno speciale simbolo integrato).
- Quando
for..ofinizia, prova a chiamare questo metodo (o ritorna un errore se non lo trova). Il metodo deve ritornare un iteratore – un oggetto con il metodonext. - La possibilità di avanzare di
for..offunziona solamente con l’oggetto ritornato. - Quando
for..ofvuole il prossimo valore, chiamanext()su quell’oggetto. - Il risultato di
next()deve avere la forma{done: Boolean, value: any}, dovedone=truesignifica che l’iterazione è completa, altrimentivaluedeve contenere il valore successivo.
Qui l’implementazione completa per range:
let range = {
from: 1,
to: 5
};
// 1. la chiamata a for..of inizialmente chiama questo
range[Symbol.iterator] = function() {
// ...ritorna l'oggetto iteratore:
// 2. Da qui in poi, for..of lavora solamente con questo iteratore, richiedendogli il prossimo valore
return {
current: this.from,
last: this.to,
// 3. next() viene invocato ad ogni iterazione del ciclo for..of
next() {
// 4. dovrebbe ritornare il valore sotto forma di oggetto {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// ora funziona !
for (let num of range) {
alert(num); // 1, poi 2, 3, 4, 5
}
Da notare la caratteristica fondamentale degli oggetti iterabili: un importante separazione di concetti:
- Il
rangestesso non possiede un metodonext(). - Invece, un altro oggetto, detto “iteratore”, viene creato dalla chiamata
range[Symbol.iterator](), e gestisce l’intera iterazione.
Quindi, l’oggetto iteratore è separato da quello su cui itera.
Tecnicamente, per rendere il codice più semplice, potremmo unirli e utilizzare range stesso.
Come nel seguente codice:
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let num of range) {
alert(num); // 1, poi 2, 3, 4, 5
}
Ora range[Symbol.iterator]() ritorna l’oggetto range stesso: questo ha il metodo next() necessario e memorizza il progresso dell’iterazione in this.current. Più corto? Sì. E molte volte può andare bene.
Il lato negativo è che ora è impossibile avere due cicli for..of che iterano sull’oggetto contemporaneamente: infatti condividerebbero lo stesso stato di iterazione, poiché c’è solo un oggetto iteratore – l’oggetto stesso. In ogni caso due cicli for..of simultanei sono molto rari, persino in alcuni scenari asincroni.
Sono possibili anche iteratori infiniti. Ad esempio, l’oggetto range diventa infinito quando range.to = Infinity. Oppure possiamo creare un oggetto iterabile che generi un’infinita sequenza di numeri pseudo-casuali.
Non c’è alcun limite per next: può ritornare più e più valori.
Ovviamente, il ciclo for..of diventerebbe infinito. Possiamo comunque fermarlo con break.
Le stringhe sono iterabili
Gli array e le stringhe sono gli oggetti su cui si utilizzano di più gli iteratori.
Per una stringa, for..of itera sui caratteri:
for (let char of "test") {
// attivato 4 volte: una volta per ogni carattere
alert( char ); // t, poi e, poi s, poi t
}