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!

28 giugno 2010

Ciao mondo!!

Filed under: Senza categoria — MaxArt @ 09:19
Beh, che dire? Anche questo blog lo inizio con il titolo “Ciao mondo!”? In effetti, trattandosi di un blog di carattere informatico, dovrebbe essere la soluzione migliore. Ora spiego di cosa si dovrebbe occupare questo sito.

Da circa un anno mi sto occupando di sviluppo web, lato client (soprattutto) e qualcosa lato server. Partendo da zero o quasi (conoscendo HTML e avendo disperse nella memoria le nozioni di JavaScript), credo di aver imparato una discreta quantità di cose, affinato grandemente il mio modo di programmare, avuto idee valide. Questo blog è dedicato a chi intende sviluppare per il web, partendo da una certa base di conoscenze HTML, CSS e JavaScript.

L’obiettivo è lo sviluppo di soluzioni web originali, partendo cioè dagli strumenti di base che mette a disposizione ogni browser o http server, senza far quindi uso di librerie come jQuery. Verranno sviluppati alcuni strumenti “di base” molto comuni, che potrei usare sistematicamente nella scrittura del codice, come ad esempio la funzione $ (dollaro) definita come

function $(obj) {
	return typeof obj==="string" ? document.getElementById(obj) : obj;
}

Spero che il significato sia chiaro. Si tratta di una funzione molto semplice, che rallenta ben poco l’esecuzione del codice ma che soprattutto aiuta molto nella stesura del codice, evitando di digitare un bel po’ di caratteri ogni volta che si cerca un elemento col suo id.

Saranno affrontati i problemi di compatibilità tra browser e risolti quando possibile. Saranno affrontate tematiche non solo strettamente programmatorie, ma anche delle direzioni dello sviluppo del web in generale. Probabilmente tradurrò qualche articolo in inglese, semmai volessi avere pareri anche da una comunità più vasta di quella italiana.

Non credo di dover aggiungere altro, dunque… buona lettura!

Blog su WordPress.com.