streams Javascript

elaborare vettori in JS

Nell'elaborare una sequenza di dati più o meno lunga JavaScript ci offre delle possibilità per poterlo fare in maniera più comoda e magari anche più intuitiva, quelle che vedremo sono tutte operazioni che si possono fare con le classiche strutture per il controllo del flusso (if e for ad esempio) ma che con i metodi dell'oggetto Array si possono fare diversamente. Qui vedemo sono alcuni modi per utilizzare i metodi più comuni, per moltissime ulteriori informazioni è possibile visualizzare la pagina di Array su MDN.

Per semplificare la trattazione definiamo subito tre variabili che utilizzeremo in tutti gli esempi:


let numeri = [1,12,7,4,3,10];
let oggetti = [
    {testo:"hello",valore:4},
    {testo:"map",valore:3},
    {testo:"reduce",valore:8}
];
let contenitore = document.getElementById("selectNomi");

un vettore di numeri, uno di oggetti e un oggetto del DOM (un select nella pagina!).

Elaborarare tutti gli elementi di un vettore

Stampare tutti i numeri contenuti in un vettore sulla console

Iniziamo da una cosa che capita di fare spesso: voglio fare una qualche operazione su tutti gli elementi di un vettore uno per uno, ad esempio stamparli sulla console:


for(let i=0 ; i<numeri.length ; i++){
    console.log(numeri[i]);
}

La parte di codice precedente fa il suo lavoro, però molti linguaggi attuali offrono la possibilità di lavorare sugli stream cioè di immaginare non un vettore di cui si guarda una casella alla volta ma un flusso di elementi che arrivano uno dietro l'altro (alla fine l'indice dove stanno nel vettore importa poco), in pratica lo stesso lavoro possiamo farlo così:


numeri.forEach( (x) => { console.log(x); });

Parafrasi: Per ogni elemento x che sta nell'Array numeri esegui console.log(x).

Forse è il caso di richiamare la sintassi di "=>", quella che viene specificata nelle parentesi tonde del forEach è una funzione che verrà chiamata per ogni elemento del vettore x passandogli il valore dell'elemento come parametro, cioè la parte nelle graffe viene eseguita una volta per il numero 1, una volta per il 12, una volta per il 7 e così via.

È una versione contratta (e magari anche più leggibile) di:


function elabora(x){
    console.log(x);
}
numeri.forEach( elabora );

Creare gli option per il select

I dati li prendiamo dal vettore di oggetti chiamato oggetti.


for(let i=0 ; i<oggetti.length ; i++){
    let elemento = document.createElement("option");
    elemento.innerText = oggetti[i].testo;
    elemento.value = oggetti[i].valore;
    contenitore.appendChild(elemento);
}

Usando il forEach() viene così:


oggetti.forEach( e => {
    let elemento = document.createElement("option");
    elemento.innerText = e.testo;
    elemento.value = e.valore;
    contenitore.appendChild(elemento);
});

Filtri

Può capitare che dei dati che ci arrivano non ci serva di utilizzarli tutti ma soltanto una parte, parte che possiamo selezionare utilizzando opportuni criteri. Vediamo come si può procedere seguendo i due diversi approcci e lavorando al solito su numeri e oggetti

Stampare i soli numeri pari


for(let i=0 ; i<numeri.length; i++){
    if(numeri[i]%2==0){
        console.log(numeri[i]);
    }
}

Oppure posso usare il metodo filter() che prende come parametro una funzione che usa appunto da filtro: gli viene passato un elemento alla volta e se il risultato della funzione (un test solitamente) è true prende l'elemento altrimenti lo scarta


numeri.filter(n => n%2==0 ).forEach( x => { console.log(x); });

Qui sopra filter() prende gli elementi pari che vengono poi stampati con il forEach.

Trasformare qualcosa in qualcos'altro

Questa strategia (parte del pattern conosciuto come map/reduce) è utile quando si vuol trasformare un qualcosa in qualcos'altro, forse più facile da capire con un paio di esempi come al solito.

Il doppio di ciascun elemento di un vettore


let risultato = [];
for(let i=0;i < numeri.length;i++){
    risultato.push(numeri[i]*2);
}

Quella che segue è la versione che usa map().

let risultato = numeri.map( n => n*2 );

Creare degli option

Con lo stesso schema possiamo creare gli elementi option per un select, il vantaggio dal punto di vista della lunghezza del codice è minore, ragiona così: da ogni elemento del vettore oggetti creo il corrispondente Node (un elemento della pagina HTML) poi un elemento alla volta lo aggiungo al select.


oggetti.map( elemento => {
    let n = document.createElement("option");
    n.innerText = elemento.testo;
    n.value = elemento.valore;
    return n;
}).forEach( x => contenitore.appendChild(x));

Creare dei riassunti

L'altra parte del pattern map/reduce prevede di creare un unico oggetto alla fine del lavoro, proviamo a vedere ad esempio come sommare un vettore:


let somma = 0;
for(let i=0 ; i<numeri.length ; i++){
    somma = somma+numeri[i];
}

E la versione aggiornata:


let somma = numeri.reduce( (somma, v) => somma+v, 0);

Il metodo reduce() prende due parametri:

(somma, v) => somma+v
una funzione a cui passa due valori: una variabile detta accumulatore cioè quella che serve a fare la sintesi (la somma nel nostro caso) e l'elemento dello stream, il risultato di questa funzione finisce di nuovo nell'accumulatore
0
il valore da dare all'inizio all'accumulatore (0 nel nostro caso perché facciamo una somma)

Tutto insieme

Vediamo un esempio pratico completo: mi arriva un insieme di dati (un vettore!) e io voglio creare una select utilizzandone soltanto alcuni (quelli che hanno un valore inferiore a cinque). Questa volta però creiamo una stringa da inserire nel select con innerHTML


let stringa = "";
for(let i=0 ; i<oggetti.length ; i++){
    if(oggetti[i].valore<5){
        stringa+=`<option value=${oggetti[i].valore}>${oggetti[i].testo}</option>`;
    }
}
contenitore.innerHTML = stringa;

contenitore.innerHTML = oggetti
.filter( elemento => elemento.valore<5 )
.map(elemento => `<option value=${elemento.valore}>${elemento.testo}</option>`)
.join();

Il join() congiunge tutte le stringhe in una sola stringa (un modo sbrigativo per fare un reduce).