CogitoWeb

3 luglio 2010

addEvent: una (altra) soluzione / a(nother) solution

Filed under: JavaScript — MaxArt @ 14:36
Tags: ,
English Version English version 

Una delle differenze più importanti tra il codice JavaScript che si scrive per Internet Explorer e quello per un browser aderente alle direttive W3C consiste nei metodi che si usano per definire un evento su un elemento della struttura della pagina. Mentre Internet Explorer usa i metodi attachEvent e removeEvent, gli standard indicano la strada in addEventListener e removeEventListener, rispettivamente, come funzioni per aggiungere e togliere un evento. Al di là delle differenze di funzionamento degli eventi tra i browser (Internet Explorer non supporta l’event capturing, motivo per il quale, del resto, è una tecnica poco sfruttata), i due metodi fanno pressoché la stessa cosa. Ma questo non vuol dire che fanno la stessa cosa.

Quello che si vorrebbe, magari, è una funzione che possa utilizzare l’uno o l’altro metodo in automatico a seconda del supporto che viene fornito dal browser. Una soluzione largamente adottata fu fornita da Scott Andrew LePera oltre 9 anni fa:

function addEvent(obj, evType, fn, useCapture){
  if (obj.addEventListener){
    obj.addEventListener(evType, fn, useCapture);
    return true;
  } else if (obj.attachEvent){
    var r = obj.attachEvent("on"+evType, fn);
    return r;
  } else {
    alert("Handler could not be attached");
  }
}

Questa semplice soluzione, insieme con la controparte removeEvent, funziona discretamente fino ad un certo punto. La differenza tra addEventListener e attachEvent consiste nel fatto che, nella funzione passata come gestore dell’evento, secondo gli standard la parola chiave this deve riferirsi, come logica vorrebbe, all’elemento HTML su cui è definito l’evento, ma in Internet Explorer this fa riferimento all’oggetto window. Una bella seccatura!

Un primo approccio alla soluzione

Una soluzione consiste nell’usare un wrapping del gestore dell’evento nel caso di Internet Explorer:

...
  } else if (obj.attachEvent){
    var r = obj.attachEvent("on"+evType, function() {fn.call(obj)});
  ...

Grazie all’uso di call, siamo sicuri che this all’interno della funzione farà riferimento all’elemento su cui è definito l’evento e non all’oggetto window. Basta questo? Macché! Questo può bastare finché gli eventi così definiti non debbano essere in seguito eliminati con removeEvent, perché il codice relativo è questo:

  obj.detachEvent("on"+evType, fn);

Ebbene, la rimozione dell’evento non funzionerà, perché si cercherà di rimuovere la funzione fn come gestore, ma non è tale funzione ad essere stata definita come gestore dell’evento, bensì il suo wrapper function() {fn.call(obj)}! Dovremmo quindi dare il riferimento al wrapper della funzione, ma questo ci è impossibile una volta usciti dalla funzione addEvent.

L’idea di John Resig

A tale proposito, il guru Peter-Paul Koch, autore del fenomenale (per quantità di risorse) sito quirksmode.org, indispensabile per aiutare tutti gli sviluppatori del web a districarsi tra le differenze di comportamento dei più comuni browser, nel 2005 indisse un concorso per definire un’accoppiata di funzioni addEvent/removeEvent che risolvesse la questione di this in Internet Explorer.

Il concorso fu vinto da un altro guru, John Resig, l’autore della notissima libreria jQuery. Questo è il codice che ha usato:

function addEvent( obj, type, fn ) {
  if ( obj.attachEvent ) {
    obj['e'+type+fn] = fn;
    obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
    obj.attachEvent( 'on'+type, obj[type+fn] );
  } else
    obj.addEventListener( type, fn, false );
}
function removeEvent( obj, type, fn ) {
  if ( obj.detachEvent ) {
    obj.detachEvent( 'on'+type, obj[type+fn] );
    obj[type+fn] = null;
  } else
    obj.removeEventListener( type, fn, false );
}

Comincio col dire che la soluzione, ovviamente, funziona benissimo. Anche in questo caso si va costruire un wrapper della funzione originale (obj['e'+type+fn]), che viene però richiamato come funzione locale. Ho però alcune osservazioni a riguardo:

  • le funzioni eseguono un controllo sulla presenza di attachEvent/detachEvent ogni volta che vengono chiamate: questo è superfluo, perché è chiaro se tali metodi saranno presenti una volta, lo saranno sempre (non è che il motore JavaScript cambia nel bel mezzo dell’esecuzione!)
  • in ogni caso viene effettuato prima un controllo su attachEvent e non su addEventListener, come sarebbe più logico (prima gli standard);
  • mi è sempre parso un metodo piuttosto invasivo, che “sporca” l’elemento HTML con la definizione di attributi (perché così vengono considerati da Internet Explorer i membri definiti gli elementi HTML) con nomi assurdamente lunghi, con garanzia di unicità non completa e per giunta non vengono ripuliti del tutto da removeEvent.

Si tratta, si badi, di argomentazioni abbastanza secondarie, se non la prima che può avere un certo marginale impatto prestazionale. Tuttavia, credo che si possano risolvere in toto.

Una nuova soluzione

Questa è l’alternativa che propongo:

if (document.addEventListener) {
  addEvent=function addEvent(obj, type, fn) {obj.addEventListener(type, fn, false);}
  removeEvent=function removeEvent(obj, type, fn) {obj.removeEventListener(type, fn, false);}
} else {
  addEvent = function addEvent(obj, type, fn) {
    fn[type+fn] = function() {return fn.call(obj, window.event);};
    obj.attachEvent("on"+type, fn[type+fn]);
  };
  removeEvent = function removeEvent(obj, type, fn) {obj.detachEvent("on"+type, fn[type+fn]);}
}

La differenza con la soluzione di John Resig sta nel fatto che la mia versione di addEvent va a salvare il riferimento al wrapper non sull’oggetto elemento HTML, ma sull’oggetto fn che viene passato come argomento. In questo modo, removeEvent sa sempre dove andare a prendere il wrapper e rimuoverlo.

Posso dire di essere piuttosto sorpreso di aver sviluppato qualcosa che mi sia piaciuto più della versione di un mostro sacro come John Resig, ed ogni tanto mi chiedo se ciò che ho scritto non abbia delle controindicazioni a me non visibili. Finora, però, non m’è parso.


addEvent: a(nother) solution


One of the main differences between the JavaScript code you write per Internet Explorer and the one for a W3C standard compliant browser is about the methods you use to define an event on an element of the page structure. While Internet Explorer uses the methods attachEvent and removeEvent, the standards have their way in addEventListener and removeEventListener, rispectively, ad function to add and remove an event. Leaving apart the working differences among browsers (Internet Explorer doesn’t support event capturing, and that’s actually the reasing why this technique is not used often), these two methods almost do the same thing. But this doesn’t mean that they do the same thing.

What we actually want is a function that calls one method or the other automatically, depending on the browser support. A widely used solution was given by Scott Andrew LePera more than 9 years ago:

function addEvent(obj, evType, fn, useCapture){
  if (obj.addEventListener){
    obj.addEventListener(evType, fn, useCapture);
    return true;
  } else if (obj.attachEvent){
    var r = obj.attachEvent("on"+evType, fn);
    return r;
  } else {
    alert("Handler could not be attached");
  }
}

This simple solution, together with the removeEvent counterpart, works fairly well under certain conditions. The difference between addEventListener and attachEvent is in the fact that, while running the function passed as event listener, by the standards the key word this must refer, as logic should hint, to the HTML element on which the event is defined, but on Internet Explorer this always refers to the window object. A real pain!

<h3A first approach to the solution

A solution consists of the using of a wrapper of the event listener in the Internet Explorer case:

...
  } else if (obj.attachEvent){
    var r = obj.attachEvent("on"+evType, function() {fn.call(obj)});
  ...

Thanks to call, we’re sure that this inside the function will refer to the element on which the event is defined and not to the window object. Is that enough? Not at all! This can be enough until the events defined in this way are not to be removed later by removeEvent, because this is the relative part:

  obj.detachEvent("on"+evType, fn);

Therefore, removing the event will not work, because it will try to remove the function fn as the listener, but that not the function defined as the event listener, but rather it’s its wrapper function() {fn.call(obj)}! So we should give the reference to the function wrapper, but that’s impossible once we’ve returned from the addEvent function.

John Resig’s idea

By this way, guru Peter-Paul Koch, author of the fantastic (for resource quantity) site quirksmode.org, essential to help web developers to have their way among behaviour differences of the most common browsers, in 2005 called a contest to define an addEvent/removeEvent functions couple to solve the this problem in Internet Explorer.

Not surprisingly, the contest was won by another guru, John Resig, author of the arch-famous library jQuery. This is the code he used:

function addEvent( obj, type, fn ) {
  if ( obj.attachEvent ) {
    obj['e'+type+fn] = fn;
    obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
    obj.attachEvent( 'on'+type, obj[type+fn] );
  } else
    obj.addEventListener( type, fn, false );
}
function removeEvent( obj, type, fn ) {
  if ( obj.detachEvent ) {
    obj.detachEvent( 'on'+type, obj[type+fn] );
    obj[type+fn] = null;
  } else
    obj.removeEventListener( type, fn, false );
}

Let’s start to say that the solution, obviously, works just fine. In this case, too, we build a wrapper of the original function (obj['e'+type+fn]), which is instead called as a “local” method. I have some notes about it:

  • those functions checks the presence of addEvent/removeEvent every time they’re called: this is redundant, because it’s obvious that if those methods are definded once, they always are (you don’t change the JavaScript engine in the middle of the execution!)
  • anyway, attachEvent is checked first, not addEventListener, as logic would suggest (standards first);
  • it always seemed to me an obtrusive way, that “spoils” the HTML element defining attributes (because it’s the way Internet Explorer deals with members defined on HTML elements) with ridicously long names, without the complete uniqueness warranty and, moreover, not entirely cleaned by removeEvent.

These are, mind you, quite minor arguments, with the exception of the first one which can have some marginal performance influence. Anyway, I think that they can be completely solved.

A new solution

This is the alternative I propose:

if (document.addEventListener) {
  addEvent=function addEvent(obj, type, fn) {obj.addEventListener(type, fn, false);}
  removeEvent=function removeEvent(obj, type, fn) {obj.removeEventListener(type, fn, false);}
} else {
  addEvent = function addEvent(obj, type, fn) {
    fn[type+fn] = function() {return fn.call(obj, window.event);};
    obj.attachEvent("on"+type, fn[type+fn]);
  };
  removeEvent = function removeEvent(obj, type, fn) {obj.detachEvent("on"+type, fn[type+fn]);}
}

The difference with the solution of John Resig is that my version of addEvent saves the wrapper reference not on the HTML element but rather on the fn object that is passed as argument. This way, removeEvent always knows where to get the wrapper and remove it.

I must say I’m quite surprised to have developed something that I liked more than the version of a sacred genius as John Resig, and sometimes I wonder if what I wrote has some issues that I don’t see. It didn’t seem to me… so far.

Annunci

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!

Blog su WordPress.com.