CogitoWeb

29 giugno 2010

forEach, map e gli altri

Filed under: JavaScript — MaxArt @ 12:32
Tags: ,
Indice:

Molto spesso vi sarà capitato, programmando in JavaScript, di avere un array e di doverlo “scorrere” per vari motivi, come quello di cercare un elemento particolare, oppure di creare un nuovo array dai suoi elementi, o ancora di eseguire una serie di istruzioni per ognuno degli elementi. Il modo classico di agire è piuttosto diretto:

for (var i=0; i<a.length; i++) {
   ...
   if (a[i]===test) ...
   ...
}

Questo modo di agire è piuttosto banale. Anzi, direi troppo, da risultare alquanto noioso e banale da implementare. A questo sembrano aver pensato gli sviluppatori di Mozilla quando hanno presentato, nelle loro estensioni di JavaScript 1.6, alcune funzionalità aggiuntive al prototype dell’oggetto Array. Vediamole nel dettaglio:

  • forEach: esegue un’azione per ognuno degli elementi dell’array;
  • map: restituisce un nuovo array costruito a partire degli elementi dall’array iniziale;
  • filter: restituisce un nuovo array formato dai soli elementi che rispettano una data condizione
  • every: restituisce un valore booleano se tutti gli elementi dell’array rispettano una certa condizione.
  • some: restituisce un valore booleano se almeno un elemento dell’array rispetta una certa condizione.
  • indexOf: restituisce la posizione di un elemento all’interno di un array (-1 se non è presente);
  • lastIndexOf: come sopra, ma partendo dal fondo dell’array

Si tratta di metodi molto comodi ma, come vedremo in seguito, da usare con la dovuta cautela perché pagano un certo scotto prestazionale.

Funzionamento Torna in cima

Come funzionano questi metodi? Prendiamo un esempio con map:

var a=[1,2,3,4,5];
var b=a.map(function(v) {return v*v;}); // b=[1,4,9,16,25];

Quello che si deve fornire a map è una funzione, che accetti come argomento l’elemento dell’array, e che restituisca un valore elaborato per costruire un nuovo array. In questo caso, è una banale funzione che restituisce il quadrato del numero. Non dobbiamo più cioè fare una roba del genere:

var a=[1,2,3,4,5];
for (var i=0, b=new Array(a.length); i<a.length; i++) {
   b[i]=a[i]*a[i];
}

Non dobbiamo più tirarci indietro la variabile i ed indicizzare. Basta con incrementi, confronti e inizializzazioni di cicli for: fa tutto map, passando comodamente l’elemento alla funzione.

L’unica cosa seccante è dover definire una funzione ogni volta, ma la cosa sarebbe stata ancora più banale se, al posto del quadrato del numero, avessimo voluto la radice quadrata, nel qual caso avremmo potuto semplicemente fare:

b=a.map(Math.sqrt);

A volte, però, l’indice dell’elemento può tornarci utile. Niente paura, perché map alla funzione indicata passa non solo l’elemento, ma appunto anche il suo indice e pure il puntatore all’array stesso:

var a=[1,2,3,4,5];
var b=a.map(function(v, i, a) {return v+i+a.length;}); // b=[6,8,10,12,14]

Inoltre, come secondo argomento di map, è possibile passare un oggetto che, all’interno della funzione, può essere referenziato con la parola this, che normalmente fa riferimento all’oggetto window.

In maniera del tutto simile agisce anche forEach, che però non restituisce alcun array e dunque non è necessario che la funzione passata restituisca un valore. Per metodi filter, every e some, invece, la funzione deve restituire un valore vero o falso (ma non necessariamente true o false) a seconda che l’elemento rispetti o meno una data condizione definita dall’utente. Allora filter restituirà un array composto solo dagli elementi che soddisfano la condizione, mentre every e some dicono (restituendo un valore booleano) rispettivamente se tutti o almeno un elemento rispetta la condizione.

Si badi che forEach, map, filter, every e some chiamano la funzione argomento solo l’elemento è effettivamente definito (e questo vuol dire che può assumere anche il valore undefined, ma l’elemento dev’essere proprio definito come… undefined!). La cosa si nota nel seguente esempio:

var a=new Array(5); // a.length=5
var b=a.map(function() {return 0}); // b=[]

Ci saremmo potuti aspettare che b diventasse un array di 5 zeri, ed invece rimane vuoto, nonostante a sia “lungo” 5. Usando un ciclo for e confrontando con la lunghezza di a, avremmo avuto il nostro array di 5 zeri: si tenga a mente questa differenza.

Altri metodi molto utili sono gli analoghi delle stringhe indexOf e lastIndexOf: essi agiscono in maniera del tutto simile ai metodi delle stringhe, cercando l’elemento indicato come argomento scorrendo array indice per indice, e restituendo -1 se l’elemento non è presente nell’array. Come secondo argomento, si può fornire l’indice da cui partire (che può anche essere negativo, ma con alcune limitazioni che mi lasciano un po’ perplesso). Esempi:

var a=["a","e","i","o","u","a","e"];
var b=a.indexOf("i");       // b=2
var b=a.indexOf("i",3);     // b=-1
var b=a.indexOf("a");       // b=0
var b=a.lastIndexOf("a");   // b=5
var b=a.lastIndexOf("a",3); // b=0

Quando si possono usare? Torna in cima

Come detto, queste novità sono state introdotte dalla Mozilla Foundation con il loro sviluppo a JavaScript 1.6: questo vuol dire supporto per tutti i browser con motore Gecko 1.8b2 e successivi. In parole povere, Firefox 1.5 e successivi (e qualche altro browser).

Tuttavia, queste estensioni sono state velocemente adottate anche dagli altri browser, come Chrome, Safari e Opera. L’unico importante browser che rimane fuori è, in effetti, il solo Internet Explorer, anche nella versione 8. E non sarebbe neanche scorretto, visto che non si tratta di estensioni standard (se non in ECMAScript versione 5, uscita solo pochi mesi fa).

Per compatibilità con gli altri browser, si può estendere preventivamente l’oggetto Array.prototype, nei modi scritti molto chiaramente nelle pagine della Mozilla Foundation:

Una volta introdotte le istruzioni in questo modo, anche Internet Explorer le supporterà in maniera del tutto identica a Firefox e agli altri browser (con qualche scotto da pagare in termini di prestazioni, come sempre per Internet Explorer).

Un trucchetto Torna in cima

Queste estensioni sono già abbastanza belle di per sé, ma forse qualcuno avrà notato che, una volta iniziato il ciclo con forEach o con map, non è possibile interromperlo, come invece spesso vorremmo. In effetti, queste funzioni sono utili soprattutto se la condizione del ciclo for che useremmo al loro posto è il semplice confronto con la lunghezza dell’array, e nient’altro.

Tuttavia, almeno per forEach, si può fare qualcosa: si userà al suo posto some, e la funzione passata, oltre a fare tutte le operazioni che vogliamo, restituirà un valore “vero” quando vogliamo che s’interrompa il ciclo. Si noti quest’esempio:

var a=[1,1,2,3,5,8,13,21,34,55], t=0;

// Questo è il ciclo for che useremmo di solito
for (var i=0; i<a.length; i++) {
   if (a[i]>10) break;
   document.write(a[i]+"\n");
   t++;
} // t=6

// Usando forEach, dobbiamo complicare la funzione, che verrà chiamata
// inutilmente per diverse altre volte (si noti la condizione dell'if)
a.forEach(function(v) {
   if (v<=10) document.write(v+"\n");
   t++;
}); // t=10

// Con some, invece, il ciclo viene interrotto prima
a.some(function(v) {
   if (v>10) return true;
   document.write(v+"\n");
   t++;
}); // t=6

Con qualche accorgimento, si può usare every anziché some. Per avere un comportamento simile con map, le cose si complicano un pelo e soprattutto la differenza col buon vecchio ciclo for diventa esigua:

var a=[1,1,2,3,5,8,13,21,34,55];

// Algoritmo che useremmo di solito
var b=[];
for (var i=0; i<a.length; i++) {
   if (a[i]>10) break;
   b[i]=a[i]*a[i];
}

// Algoritmo con some
var b=[];
a.some(function(v, i) {
   if (v>10) return true;
   b[i]=v*v;
});

Prestazioni Torna in cima

Quando si vanno ad esaminare le prestazioni, come avevo accennato, sorgono i problemi. Facendo alcuni test con vari browser, sotto le medesime condizioni ho provato a confrontare i seguenti costrutti equivalenti:

var a=new Array(1000);
for (var i=0; i<a.length; i++) a[i]=0; // Inizializzazione di a;

// Ciclo for
for (var i=0; i<a.length; i++) a[i]=a[i]*i*Math.random();

// map
a.map(function(v, i) {return v*i*Math.random()});

Ebbene, i confronti sono impietosi. Eseguendo qualcosa come 4000 iterazioni con map, questi sono i risultati sui vari browser:

  • Firefox 3.6.6: 2459 ms
  • Chrome 6.0.427.0: 463 ms
  • Opera 10.54: 3266 ms
  • Internet Explorer 7/8: circa 15000 ms

Usando invece il semplice ciclo for, i tempi sono talmente brevi da risultare non rilevabili, in tutti i browser (incluso Internet Explorer). Come mai questo fatto?

A ben pensarci, la cosa è semplice: con map ad ogni ciclo viene effettuata una chiamata alla funzione che viene passata, e questa è un’operazione piuttosto costosa a livello di prestazioni. Il mio consiglio, quindi, è quello di usare forEach e gli altri metodi solo quando:

  • la chiamata alla funzione è temporalmente molto più breve rispetto all’esecuzione della funzione stessa;
  • il ciclo è di lunghezza breve, al massimo qualche migliaio di iterazioni;
  • il tempo di esecuzione non è così importante (nei casi, ad esempio, di dipendenza dall’utente)
  • quando si stende una versione preliminare del codice, per velocità di scrittura, procedendo in un secondo momento a sostituire i metodi con dei cicli for.

Detto questo, buon codice a tutti!

Annunci

Blog su WordPress.com.