Questa è la traduzione dell'articolo Seven JavaScript Techniques You Should Be Using Today di Dustin Diaz pubblicato originariamente su Digital Web Magazine il 23 aprile 2007. La traduzione viene qui presentata con il consenso dell'editore e dell'autore.
Il fatto che il codice Javascript che producete sia o meno mantenibile dipende spesso dal compito che stato cercando di svolgere. Potete poi seguire una serie di best practices o meno, e in ogni caso vi troverete bene. A prescindere da tutto ciò, in questo articolo mostreremo sette tecniche che potrete aggiungere alle vostre abitudini nella scrittura di codice per diventare programmatori migliori.
Una cosa da tenere in mente man mano che affronterete questi esempi è che ci sono tanti altri modi per svolgere questi compiti. Il nostro obiettivo sarà semplicemente quello di mostrare in maniera più chiara come certe cose possano essere fatte in maniera più intelligente. I benefici di ciascun metodo dovrebbero essere autoevidenti, ma alla fine lo scopo vero è quello di imparare un modo più semplice per identificare gli errori fatti dai programmatori e dagli autori di librerie, aggiungere nuove skill al vostro bagaglio tecnico e imparare la flesibilità di questo linguaggio.
Uno: ramificare quando è possibile
Quando le prestazioni sono importanti è spesso opportuno ramificare le funzioni in modo da assicurarsi che task che sfruttano molto il processore e richiedono molta memoria non siano frequentemente ripetuti. Uno degli scenari più comuni in cui questa situazione può verificarsi è nella gestione delle differenze tra i vari browser. Diamo un'occhiata agli esempi che seguono, e vediamo come si può velocizzare la gestione di una richiesta XMLHttp e l'assegnazione degli eventi.
Nel primo esempio di codice costruiremo una funzione asyncRequest
che mette in pratica questa tecnica:
var asyncRequest = function() {
function handleReadyState(o, callback) {
var poll = window.setInterval(function() {
if(o && o.readyState == 4) {
window.clearInterval(poll);
if ( callback ){
callback(o);
}
}
},
50);
}
var http;
try {
http = new XMLHttpRequest();
}
catch(e) {
var msxml = [
'MSXML2.XMLHTTP.3.0',
'MSXML2.XMLHTTP',
'Microsoft.XMLHTTP'
];
for ( var i=0, len = msxml.length; i < len; ++i ) {
try {
http = new ActiveXObject(msxml[i]);
break;
}
catch(e) {}
}
}
return function(method, uri, callback, postData) {
http.open(method, uri, true);
handleReadyState(http, callback);
http.send(postData || null);
return http;
};
}();
Si osservi con particolare attenzione come, attraverso l'uso delle closures, siamo stati in grado di ramificare la nostra funzionalità principale prima che una funzione venga restituita alla variabile asyncRequest
. Ciò che si guadagna è che il nostro codice non dovrà verificare se l'oggetto XMLHttpRequest è nativamente disponibile ad ogni richiesta. Eviteremo pure di incorrere in pericolosi loop nel check delle diverse versioni del controllo ActiveX per Internet Explorer. Inoltre, si osservi il modo in cui è usata la closure per incapsulare la logica del nostro script in modo tale che non andiamo a contaminare il namespace globale con variabili che servono solo a portare a termine questo specifico task.
Consideriamo ora il secondo esempio. Lo ha affrontato Dean Edwards circa un anno fa quando ha voluto condividere un piccolo tip su come velocizzare la rilevazione di un oggetto (object detection) e mette in pratica la stessa tecnica vista in precedenza per la funzione asyncRequest
applicandola al problema dell'assegnazione di listener per gli eventi.
Si badi che questa questa versione di addListener
modificherà l'ambito di this
nei vostri callback, oltre a inviare indietro l'evento appropriato come primo argomento nei browser più conformi agli standard. La funzione che ne risulta, inoltre, sarà la più veloce nella gara svoltasi per ricodificare addEvent
, semplicemente perché mette in pratica la ramificazione:
var addListener = function() {
if ( window.addEventListener ) {
return function(el, type, fn) {
el.addEventListener(type, fn, false);
};
} else if ( window.attachEvent ) {
return function(el, type, fn) {
var f = function() {
fn.call(el, window.event);
};
el.attachEvent('on'+type, f);
};
} else {
return function(el, type, fn) {
element['on'+type] = fn;
}
}
}();
Due: creare dei flag
Creare dei flag è un altro grande metodo per velocizzare la rilevazione degli oggetti. Se avete diverse funzioni che vanno a verificare la disponibilità dello stesso tipo di oggetto, allora, semplicemente, create dei flag. Per esempio, se state verificando di essere di fronte ad un browser in grado di compiere i più comuni task relativi alla manipolazione del DOM, può essere utile impostare un flag di questo tipo:
var w3 = !!(document.getElementById && document.createElement);
Oppure, se state effettuando un'operazione di browser sniffing, un modo semplice per verificare se i vostri visitatori stanno uando Internet Explorer è di controllare la presenza di oggetti ActiveX, così:
var ie = !!window.ActiveX;
Gli operatori not ( !! ) effettuano semplicemente una conversione booleana. Il primo operatore cambia il tipo di oggetto che sta alla sua destra in un valore Booleano, il secondo inverte qualunque cosa sia stata restituita dal primo.
Una cosa a cui bisogna prestare particolare attenzione è l'ambito in cui questi flag sono dichiarati. Se il vostro dominio di lavoro è un ambiente piccolo come quello di un blog, con poche interazioni Javascript, allora sarebbe opportuno e prudente dichiararli globalmente. Ma se ci si trova nel contesto di grandi applicazioni, allora può essere utile ricorrere ad un namespace per dichiarare i flag all'interno di un oggetto:
var FLAGS = {
w3: function() {
return !!(document.getElementById && document.createElement);
}(),
ie: function() {
return !!window.ActiveX;
}()
};
L'obiettivo di dichiarare i flag solo una volta è per non ridefinirli localmente in varie funzioni attraverso la vostra applicazione, senza che sia necessario duplicare il codice.
Tre: creare ponti
Siate gentili con le vostre API! Un ponte (bridge) è un modello di progettazione usato nell'ingegneria del software che viene creato per "separare un astrazione dalla sua implementazione in modo tale che le due possanno variare indipendentemente" (fonte: Wikipedia). I ponti possono esserci d'aiuto nella programmazione guidata da eventi. Se state appena muovendo i primi passi nel mondo dello sviluppo di API -si tratti di un servizio web o di qualcosa tipo getSomething e setSomething- creare dei ponti vi aiuterà a manentre il codice delle API più pulito.
Uno dei tipici casi di applicazione pratica per un ponte è per i callback di listener di eventi. Poniamo che abbiate una funzione chiamata getUserNameById
. Vogliamo che questa informazione sia rintracciata attraverso un evento click. Ovviamente, l'id dell'elemento su cui si clicca contiene l'informazione che cerchiamo. Bene, ecco il modo in cui non va fatto:
addListener(element, 'click', getUserNameById);
function getUserNameById(e) {
var id = this.id;
// do stuff with 'id'
}
Come si vede, abbiamo creato un'API tuttaltro che perfetta. Possiamo operare solo nell'ambito del callback per catturare l'id dall'oggetto this
. Invece, proviamo a fare così. Si inizia con la funzione dell'API:
function getUserNameById(id) {
// do stuff with 'id'
}
Sembra già una cosa più pratica! Ora possiamo programmare su un'interfaccia e non su un'implementazione. Tenendo questo in mente, ora possiamo creare un ponte per collegare le due funzioni:
addListener(element, 'click', getUserNameByIdBridge);
function getUserNameByIdBridge (e) {
getUserNameById(this.id);
}
Quattro: provare la delegazione degli eventi (event delegation)
La parola chiave in questo caso è try. Non si dimostra pratica in tutte le occasioni, ma vale la pena provare. Ho imparato questa cosa chattando un po' con i ragazzi del team di Yahoo! e vedendola in azione sul widget per menu su cui stavano lavorando. Il blogger e sviluppatore di Yahoo! Christian Heilmann ha affrontato il tema in un articolo intitolato Event Delegation.
La delegazione degli eventi è un modo semplice per abbreviare la gestione degli eventi. Funziona aggiungendo un listener ad un elemento contenitore e recuperando il target che ha attivato l'evento invece che assegnando diversi listener agli elementi figli e accedendo all'oggetto attraverso this
. Per fare un esempio, se abbiamo una lista non ordinata (ul
) con cinque item (li
) e vogliamo gestire l'evento click su tali elementi, è più efficiente assegnare un evento click all'intera lista (ul
) e quindi gestire il target. Ecco il codice, partendo dalla parte HTML:
<ul id="example">
<li>foo</li>
<li>bar</li>
<li>baz</li>
<li>thunk</li>
</ul>
Ed ecco il Javascript:
var element = document.getElementById('example');
addListener(element, 'click', handleClick);
function handleClick(e) {
var element = e.target || e.srcElement;
// do stuff with 'element'
}
Nell'esempio, quando l'utente clicca su un elemento li
è come se cliccasse sull'elemento ul
. Possiamo catturare il target originario ispezionando l'oggetto dell'evento che è stato passato in maniera invisibile al metodo di callback -nel nostro caso e (e è diventato il modo convenzionale per indicare una variabile usata per event). e contiene una proprietà chiamata target
(che è l'elemento target). In Internet Explorer la proprietà è chiamata srcElement
. Usiamo semplicemente un operatore logico OR per determinare se la proprietà target
è disponibile, altrimenti il default diventa srcElement
.
Cinque: includere metodi quando usate getElementsByQualsiasiCosa
Una delle omissioni più notevoli nelle API di quasi tutte le librerie Javascript è la mancanza della possibilità di aggiungere callback alle collezioni di funzioni di un elemento. Che si usi getElementsByClassName
, getElementsByAttribute
o un motore di query CSS, pensate alla logica che sta dietro a quello che volete fare con queste collezioni. In ogni caso, ha un senso consentire la possibilità di avere callback.
Facciamo un passo indietro e pensiamo a cosa accade dietro le scene ogni volta che vogliamo oettenere la collezione di un elemento. In genere vogliamo farci qualcosa con questo elemento. E se la nostra funzione getElementsByQualsiasiCosa consente solo di ottenere un array di elementi e di restituirli, non pensate che sia uno spreco iterare attraverso l'intera collezione di nuovo quando siete pronti ad assegnare una funzione a questi elementi?
Iniziamo con un'API standard per ottenere elementi nel DOM basandosi su un selettore CSS:
function getElementsBySelector(selector) {
var elements = [];
for ( ... ) {
if ( ... ) {
elements.push(el);
}
}
return elements;
}
Ora che abbiamo un array di elementi corrispondenti, vogliamo assegnare ad essi qualche funzionalità:
var collection = getElementsBySelector('#example p a');
for ( var i = collection.length - 1; i >=0; --i ) {
// do stuff with collection[i]
}
Alla fine di questo processo abbiamo eseguito due loop quando ne basterebbe uno soltanto. Tenendo ciò in mente, possiamo modificare la nostra funzione per registrare il callback della funzione come parte del loop iniziale attraverso gli elementi corrispondenti:
function getElementsBySelector(selector, callback) {
var elements = [];
for ( ... ) {
if ( ... ) {
elements.push(el);
callback(el);
}
}
return elements;
}
getElementsBySelector('#example p a', function(el) {
// do stuff with 'el'
});
Sei: incapsulare il codice
È una cosa vecchia, eppure raramente messa in pratica. Quando state implementando del Javascript che sta diventando voluminoso e difficile da gestire, la cosa migliore per proteggere il codice e lasciare che esso si integri al meglio con il resto è usare una closure. Invece di avere una pagina piena di variabili globali:
var a = 'foo';
var b = function() {
// do stuff...
};
var c = {
thunk: function() {
},
baz: [false, true, false]
};
aggiungiamo una closure:
(function() {
var a = 'foo';
var b = function() {
// do stuff...
};
var c = {
thunk: function() {
},
baz: [false, true, false]
};
})();
La ragione per cui una closure protegge il vostro codice è che da al codice stesso un ambito chiuso che non consente a predatori vari di rovinare tutto. Una closure stabilisce uno spazio in cui ogni variabile definita all'interno di quello spazio non è accessibile dall'esterno. Ciò include anche le funzioni stesse. Provate il seguente esempio per verificare:
// global land has access to ONLY a, but cannot execute code within it
function a() {
// has access to a, and b, but cannot execute code within b, c, and d
function b() {
// has access to a, b, and c, but cannot execute code within c, and d
function c() {
// has access to a, b, c, and d but cannot execute code within d
function d() {
// has access to all, and the ability to execute all
}
}
}
}
Sette: reinventare la ruota
Ultima ma non meno importante: reinventate la ruota. OK, non è una tecnica intelligente, non ha a che fare con il codice, è solo un incoraggiamento a non vergognarsi a provare una cosa completamente nuova quando si lavora con Javascript.
Spesso, quando lavoro su un prototipo o costruisco un widget, sono sempre contento se qualcuno ha già fatto quello che io sto tentando di fare. Eppure, vale la pena provare a vedere se posso fare meglio. Non tutte le ruote sono rotonde. Con un linguaggio tanto flessibile l'immaginazione può portarvi davvero tanto avanti.