Tutorial Javascript parte II

Nella prima parte di questo tutorial JavaScript abbiamo visto i costrutti principali del linguaggio. In questo post vedremo varie caratteristiche del linguaggio JavaScript arrivando a parlare di JavaScript object oriented. Alla fine di questa mini guida al linguaggio JavaScript vedremo la soluzione al mini quiz visto all’inizio della prima parte, ve lo ricordate? Sapete già la soluzione?

Scope di una function

Una function può ovviamente essere definita all’interno di un oggetto: in tal caso si può accedere ai campi dell’oggetto che la contiene tramite la variabile this:

var persona = {  
    dataDiNascita: new Date(1970, 1, 1),  
    getEta: function() {  
        var diff = new Date() - this.dataDiNascita;  
        var diffInAnni = diff / 1000 / 60 / 60 / 24 / 365;  
        return Math.floor(diffInAnni);  
    }  
};

Un concetto importante è quello di callback: una function può essere passata come parametro di un metodo. Ad esempio i metodi dei vari framework che effettuano una chiamata ajax prendono solitamente fra i parametri anche una function da eseguire dopo la risposta del server. Vediamo un esempio semplificato:

function eseguiChiamata(parametri, success, failure) {  
    //esegue la chiamata al server  
    var risposta = chiamaServer(parametri);  
    //se viene ritornato qualcosa viene invocata la function success  
    if (risposta) {  
        success(risposta);  
    }  
    else {  
        //altrimenti viene invocata la function failure  
        failure(risposta);  
    }  
}

Ma cosa succede se una function viene invocata direttamente senza passare dall’oggetto di appartenenza, quindi con metodo(parametri) e non con oggetto.metodo(parametro)? In questi casi this contiene il riferimento all’oggetto globale window e non quello che ci aspettiamo. Per capire meglio questo concetto vediamo un esempio di una function molto utile che molti framework definiscono:

function each(array, fn) {  
    if (array != null) {  
        //se non passo un array creo un array al volo
        //con un unico elemento  
        if(typeof array.length == "undefined" || 
                typeof array == "string") {  
            array = [array];  
        }  
        //itero sull'array e per ogni oggetto
        //richiamo la function passata  
        for(var j = 0, len = array.length; j < len; j++) {  
            fn(array[j], j, array);  
        }  
    }  
}

Questa function prende come parametro un array e una function da chiamare su ogni elemento, se il primo parametro non è un array viene creato al volo un array di un solo elemento. La function passata come parametro viene richiamata con tre parametri: l’i-esimo elemento, l’indice i e l’array completo. Usiamo la function each per calcolare la somma degli elementi di un array di numeri: per fare ciò creiamo un oggetto sommatore che ha al suo interno il totale e un metodo somma che viene passato come parametro alla function each.

var a = [1,2,3];  
var sommatore = {  
    totale: 0,  
    somma: function(v) {  
        this.totale += v;  
    }  
};  
each(a, sommatore.somma);  
sommatore.totale == 0

In questo caso sommatore.totale non contiene il valore aspettato, cerchiamo di capire perchè. Tutti le function (essendo oggetti) hanno due importanti function utilizzabili: apply e call. Tutte e due richiamano la function impostando lo scope: in pratica lo scope corrisponde all’oggetto che all’interno della function può essere richiamato con la variabile this. La differenza fra i due metodi è che apply prende come parametri lo scope e un array di parametri da passare mentre call prende lo scope e i parametri da passare (non come array). Modifichiamo il nostro metodo each per passare lo scope:

function each(array, fn, scope)  
{  
    if (array != null) {  
        if(typeof array.length == "undefined" || 
                typeof array == "string") {  
            array = [array];  
        }  
        for(var j = 0, len = array.length; j < len; j++) {  
            //lo scope è opzionale, se non viene passato 
            //viene usato l'elemento stesso come scope  
            fn.call(scope || array[j], array[j], j, array);  
        }  
    }  
}

Passando anche lo scope al metodo each si ottiene il risultato aspettato:

each(a, sommatore.somma, sommatore);  
sommatore.totale == 6  

Classi e istanze

Fino ad adesso abbiamo visto solo oggetti definiti al volo, ma in JavaScript è possibile creare anche classi e istanze di classi in stile Java. Per fare ciò è fondamentale capire il concetto di costruttore. Un costruttore è una normale function JavaScript (di solito definita con il nome in upper camel case) in cui viene eseguita la costruzione di un oggetto.

function Persona(nome, cognome, dataDiNascita) {  
    //inizializzo i campi  
    this.nome = nome;  
    this.cognome = cognome;  
    this.dataDiNascita = dataDiNascita;  
    //aggiungo alcune function  
    this.getEta = function() {  
        var diff = new Date() - this.dataDiNascita;  
        var diffInAnni = diff / 1000 / 60 / 60 / 24 / 365;  
        return Math.floor(diffInAnni);  
    };  
}

In pratica Persona può essere considerata una classe e quindi può essere istanziata con una new: eseguendo la new di un costruttore viene ritornato l’oggetto corrispondente al costruttore stesso a meno che non ci sia un return esplicito.

var p = new Persona('Mario', 'Rossi', new Date(1970, 1, 1));  
p.getEta() == 41

In JavaScript è possibile aggiungere metodi a una classe anche attraverso l’oggetto prototype: in pratica quando si chiede il valore di un campo su un oggetto (quindi anche quando si cerca di invocare un metodo su un oggetto) se tale campo non viene trovato viene cercato nell’oggetto prototype. Quindi l’esempio visto sopra può essere riscritto aggiungendo la function getEta al prototype e non all’oggetto stesso (facendo così si ha anche un risparmio di memoria in quanto la function è creata una volta sola e non una volta per ogni istanza):

function Persona(nome, cognome, dataDiNascita) {  
    //inizializzo i campi  
    this.nome = nome;  
    this.cognome = cognome;  
    this.dataDiNascita = dataDiNascita;  
}  
Persona.prototype.getEta = function() {  
    var diff = new Date() - this.dataDiNascita;  
    var diffInAnni = diff / 1000 / 60 / 60 / 24 / 365;  
    return Math.floor(diffInAnni);  
};

Il campo prototype può essere usato anche per creare gerarchie di classi: per esempio possiamo creare la classe Studente che estende la classe Persona aggiungendo una nuova function getAnniDiStudio:

//costruttore della classe derivata  
function Studente(nome, cognome, dataDiNascita) {  
    Persona.call(this, nome, cognome, dataDiNascita);  
}  
//imposto il prototype a una istanza di Persona  
Studente.prototype = new Persona();  
//aggiungo un nuovo metodo al prototype di Studente
//in cui richiamo una function della classe base  
Studente.prototype.getAnniDiStudio = function() {  
    return this.getEta() - 6;  
}

Javascript è un linguaggio dinamico, attraverso il prototype permette addirittura (anche se molti sconsigliano questa pratica) di aggiungere metodi a classi standard. Vediamo un esempio in cui si aggiunge una function startsWith al prototype della classe String:

var s = 'abcd';  
String.prototype.startsWith= function(s){  
    return this.indexOf(s) == 0;  
};  
s.startsWith('ab')  

Possiamo notare che la function che abbiamo aggiunto può essere invocata su tutte le istanze di String, anche quelle create prima della definizione.

Visibilità dei campi di un oggetto

In tutti gli esempi visti fino ad ora tutti i campi degli oggetti sono pubblici, in JavaScript non esiste una esplicita definizione di visibilità di un campo (non è possibile definire un campo private o protected come in Java). Vediamo come è possibile simulare un campo privato: per esempio rendiamo la data di nascita della classe Persona privata:

function Persona(nome, cognome, dataDiNascita) {  
    //inizializzo i campi  
    this.nome = nome;  
    this.cognome = cognome;  
    this.getEta = function() {  
        var diff = new Date() - dataDiNascita;  
        var diffInAnni = diff / 1000 / 60 / 60 / 24 / 365;  
        return Math.floor(diffInAnni);  
    };    
}

Scrivendo così non abbiamo aggiunto un campo dataDiNascita al this, ma usiamo direttamente il valore passato come parametro all’interno del metodo getEta. Il metodo getEta è una closure, ha accesso non solo ai campi pubblici dell’oggetto a cui appartiene, ma anche ai parametri della function in cui viene definito e alle variabili locali definite all’interno della function. Il metodo getEta non è nè pubblico nè privato, viene definito come privilegiato, può accedere sia ai campi pubblici che a quelli privati. Un metodo pubblico può invece accedere solo ai campi pubblici. Un esempio un po’ ingarbugliato in cui sono usati tutti i tipi di metodi visti è il seguente:

function Costruttore(parametro) {  
    var variabilePrivata = 5;  
    this.variabilePubblica = parametro;  
    function functionPrivata() {  
        return variabilePrivata + parametro;  
    }  
    this.functionPrivilegiata = function(par) {  
        return parametro + variabilePrivata + this.variabilePubblica +   
            functionPrivata() + par + this.functionPubblica1(1);  
    }  
}  
Costruttore.prototype.functionPubblica1 = function(par) {  
    return par + this.variabilePubblica;  
}  
Costruttore.prototype.functionPubblica2 = function() {  
    return this.variabilePubblica + this.functionPrivilegiata(400);  
}  
var c = new Costruttore(10);  
c.variabilePubblica == 10  
c.functionPubblica1(100) == 110  
c.functionPrivilegiata(200) == 10 + 5 + 10 + (5 + 10) + 200 + (1 + 10)  
c.functionPubblica2() == 10 + 451

I namespace JavaScript

Come già detto memorizzare variabili e function a livello globale dell’oggetto window è sconsigliato. Questo perchè se si utilizzano librerie esterne si può incorrere in problemi di collisioni di nomi che portano a bug difficili da trovare. Per evitare ciò si usa il concetto di namespace che risulta molto simile a un package Java. Un namespace non è altro che un oggetto JavaScript in cui vengono aggiunte variabili, function o ulteriori namespace. Un esempio è il seguente:

var it = {};  
it.cosenonjaviste = {};  
//aggiungo una classe al namespace  
it.cosenonjaviste.OggettoBase = function() {  
};  
//aggiungo un'altra classe facendola derivare dalla prima  
it.cosenonjaviste.OggettoDerivato = function() {  
};  
it.cosenonjaviste.OggettoDerivato.prototype =
    new it.cosenonjaviste.OggettoBase();  
//creo un sotto namespace  
it.cosenonjaviste.util = {};  
//aggiungo una function statica  
it.cosenonjaviste.util.functionStatica = function(a, b) {  
    var obj = new it.cosenonjaviste.OggettoDerivato();  
};

Qualche esempio complesso

Vediamo adesso alcuni modi di usare javascript che a prima vista sembrano incomprensibili. Per esempio alcune volte viene definita e eseguita subito una function anonima per inizializzare dati senza “sporcare” l’oggetto globale window:

(function() {  
    var s1 = 10;  
    var s2 = 20;  
    oggettoGlobale.valore = s1 + s2;  
})();

Una cosa simile può essere usata anche per creare una function come risultato di una function. Vediamo come esempio un oggetto che contiene una function creaXmlHttpRequest creata all’interno di una function anonima eseguita subito. In pratica quando viene creato l’oggetto viene subito invocata la function anonima e il risultato (dipendente dal browser) viene memorizzato nel campo creaXmlHttpRequest.

var oggetto = {  
    creaXmlHttpRequest: (function() {  
        // codice per Mozilla  
        if (window.XMLHttpRequest) {  
            return function(url) {  
                var xmlhttp = new XMLHttpRequest();  
                xmlhttp.onreadystatechange=xmlhttpChange;  
                xmlhttp.open("GET",url,true);  
                xmlhttp.send(null);  
            }  
        }  
        // codice per Internet Explore  
        else if (window.ActiveXObject) {  
            return function(url) {  
                var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");  
                if (xmlhttp) {  
                    xmlhttp.onreadystatechange=xmlhttpChange;  
                    xmlhttp.open("GET", url, true);  
                    xmlhttp.send();  
                }  
            }  
        }  
    })()  
};

Un altro esempio un po’ particolare è il seguente, in pratica si esegue un calcolo lungo in modo ritardato (ovvero solo al momento del primo utilizzo del risultato). Il risultato di tale calcolo viene memorizzato in una variabile locale immodificabile in modo che il calcolo debba essere fatto una sola volta. La prima chiamata modifica la function stessa assegnandogli una nuova function che semplicemente ritorna il valore calcolato.

function calcoloLazy() {  
    var l = 123;//potrebbe essere un calcolo lungo e complicato  
    calcoloLazy = function() {  
        return l;  
    }  
    return l;  
} 

E finalmente la soluzione al quesito

Siamo arrivati in fondo a questo tutorial sul linguaggio JavaScript, vediamo di nuovo l’esempio visto all’inizio della prima parte:

String.prototype.r = function() {
    return this.split("").reverse().join('')
}

function a(b) {
    if (arguments.length == 1) {
        return b.slice(1, 3) + b[1];
    } else if (arguments.length == 2) {
        b = arguments[0] + arguments[1];
    }
    return (b || this).r();
}

(function b() {
    alert(a.apply(" esoc") + a("mnopqrs") + a("ets", "ivaj "));
})()

Se avete seguito con attenzione questo tutorial questo codice dovrebbe sembrare chiarissimo (ora non esageriamo, diciamo che dovrebbe sembrare almeno leggibile!). Ecco la soluzione:

  • la function r è aggiunta al prototype di String e permette di invertire una stringa
  • la function a manipola la stringa passata, il comportamento dipende dal numero di parametri passati
  • la function b viene invocata immediatamente grazie alle parentesi in fondo. Stampa la concatenazione di tre stringhe ottenute con tre chiamate alla function a

In pratica eseguendo il codice viene mostrato un alert JavaScript immediatamente con scritto cose non javiste, semplice no?

Come alla fine della primo post vi consiglio l’ottimo libro JavaScript: The good parts di Douglas Crockford se volete approfondire ulteriormente l’argomento:


Fabio Collini

Software Architect con esperienza su piattaforma J2EE e attualmente focalizzato principalmente in progetti di sviluppo di applicazioni Android. Attualmente sono nel team Android di Cynny, ci stiamo occupando dello sviluppo dell'app Morphcast. Coautore della seconda edizione di Android Programmazione Avanzata e docente di corsi di sviluppo su piattaforma Android. Follow me on Twitter - LinkedIn profile - Google+