JavaScript, da strumento con cui rendere interattive le pagine Web, sta guadagnano il suo posto anche tra le tecnologie lato server, grazie a progetti come Node.js, e nel mondo mobile con Cordova/PhoneGap o Titanium.
Grazie alla sua popolarità e all'evoluzione di HTML sono state create centinaia di librerie per semplificare la programmazione in JavaScript, testimoniando l'esigenza di una evoluzione dello standard di riferimento del linguaggio: ECMAScript.
Dal luglio 2008, infatti, il comitato per la definizione dello standard ECMAScript ha avviato il progetto ECMAScript 6th Edition, detto anche ECMAScript Harmony, che introdurrà interessanti novità che si rifletteranno naturalmente nella sua implementazione più nota: JavaScript.
Allo stato attuale non è stato stabilito quando lo standard sarà definitivamente approvato, ma ormai sono abbastanza ben delineate le nuove funzionalità di cui cercheremo di dare una panoramica in questo articolo.
Un po' di storia
Prima di addentrarci nell'esplorazione delle novità che verranno introdotte in JavaScript è utile dare uno sguardo a cosa è accaduto in questi anni intorno alla definizione dello standard ECMAScript per capire come mai l'evoluzione del linguaggio ha subito un rallentamento.
Nel dicembre del 1999 è stato definita la versione 3 di ECMAScript, che rappresenta la versione attualmente supportata dalla maggior parte dei browser. Il TC39, la commissione tecnica dell'ECMA incaricata di sviluppare lo standard ECMAScript, si mise subito al lavoro per la definizione della versione successiva, ECMAScript 4.
L'obiettivo era ambizioso, ma alcuni disaccordi interni impedirono il proseguimento lineare dei lavori. Si arrivò quindi, nel luglio del 2008, ad un abbandono di quella che avrebbe dovuto essere la versione 4 di ECMAScript e ad una pianificazione meno ambiziosa dello standard. Nel dicembre 2009 si è quindi giunti ad ECMAScript 5 che presenta un insieme ridotto di novità rispetto alla precedente versione ufficiale, cioè la 3. Alla futura versione 6, che si suppone verrà pubblicata nel corso del 2013, è stato attribuito il nome in codice Harmony per far riferimento all'accordo raggiunto all'interno della commissione dopo la fase di stallo durata quasi otto anni.
1. Novità sintattiche
const
Iniziamo la nostra esplorazione con alcune delle principali novità che riguardano la sintassi di JavaScript. Ferma restando la compatibilità con il passato, tra le principali novità segnaliamo la possibilità di dichiarare costanti tramite la parola chiave const:
const piGreco = 3.14;
La dichiarazione di una costante consente di creare valori in sola lettura ed il suo utilizzo risulta abbastanza intuitivo. La sua introduzione colma una lacuna che costringeva ricorrere a variabili con il conseguente rischio di alterazione del valore durante l'esecuzione.
let
Altra novità riguarda la possibilità di creare blocchi di visibilità di una variabile (block scope) tramite la parola chiave let:
var x = 10;
let x = 20 {
console.log(x);
}
console.log(x);
In questo esempio la dichiarazione della variabile x
tramite let
nasconde la visibilità dell'omonima variabile dichiarata all'esterno tramite var
, ma soltanto all'interno del blocco di codice governato dalla parola chiave. Il risultato sarà la scrittura sulla console del valore 20
, il valore di x
assegnato da let
, seguito dal valore 10
, il valore della variabile omonima ma esterna al blocco.
for-of
Il costrutto for
si arricchisce in una nuova forma di iterazione, la cosiddetta for-of. Analizziamolo con un semplice esempio:
var myArray = [1, 2, 3];
for (let value of myArray) {
console.log(value);
}
Il risultato di questa iterazione è la visualizzazione dei valori 1, 2 e 3. Bisogna prestare attenzione a questa versione di for
poichè a prima vista potrebbe sembrare identico al costrutto for-in
. In realtà, mentre for-in
itera sugli indici dell'array (e più in generale sui nomi delle proprietà di un oggetto), for-of
itera tra i valori dell'array (e quindi sui valori delle proprietà di un oggetto).
3. Strutture dati
Alcune interessanti novità che verranno supportate dalla futura versione di JavaScript riguardano le strutture dati che fungono da collection. Di solito in JavaScript quando si ha bisogno di gestire insiemi di dati si fa ricorso agli array, ma non sempre questa struttura dati è agevole. In futuro avremo a disposizione strutture dati molto comode che ci eviteranno di inventarci artifici o di appoggiarci a routine ad hoc per fare operazioni abbastanza comuni.
Set
Tra le nuove strutture dati che avremo a disposizione c'è il Set, l'insieme. Un Set può contenere dati di qualsiasi tipo ma senza duplicati. A differenza di un comune array, le operazioni più comuni che vengono fatte sugli insiemi è l'aggiunta o la rimozione di elementi e la verifica dell'esistenza di un elemento. Analizziamo il seguente esempio:
var mySet = new Set();
mySet.add(1);
mySet.add(2);
mySet.add("tre");
console.log(mySet.size); //visualizza 3
console.log(mySet.has(2)); //visualizza true
mySet.delete(1);
console.log(mySet.has(1)); //visualizza false
for (let item of mySet)
console.log(item); //visualizza gli elementi del Set
La prima istruzione dell'esempio indica come creare un Set
mentre le istruzioni successive aggiungono elementi all'insieme tramite il metodo add()
. La proprietà size
indica il numero di elementi contenuti nell'insieme mentre il metodo has()
indica se un elemento è contenuto o meno in esso. Per eliminare un elemento dall'insieme usiamo il metodo delete()
e per eseguire un'iterazione sui suoi elementi possiamo utilizzare il costrutto for-of
che abbiamo già incontrato.
Map
Oltre agli insiemi abbiamo la possibilità di creare mappe associative che ci consentono di associare ad un valore una chiave. In realtà questo meccanismo è in qualche modo presente nell'attuale versione di JavaScript. Infatti gli oggetti JavaScript non sono altro che coppie di chiavi e valori che ne rappresentano le proprietà e proprio su questo principio si basa la notazione JSON. Tuttavia la mappa associativa implicita degli oggetti JavaScript prevede che la chiave, quindi il nome della proprietà, non possa essere qualcosa di diverso da una stringa.
La nuova struttura dati Map, invece, consente di creare associazioni tra chiavi e valori di qualsiasi tipo, come possiamo vedere nel seguente esempio:
var myMap = new Map();
myMap.set("nome", "Mario");
myMap.set(3.14, "Pi greco");
myMap.set(document.getElementById("txtCognome"), "Rossi");
Nell'esempio abbiamo creato la mappa ed aggiunto tre coppie chiave-valore tramite il metodo set()
. Come si può vedere, le chiavi, rappresentate dal primo argomento del metodo, possono essere di tipo diverso: stringhe, valori numerici, oggetti.
Analogamente agli insiemi, le mappe prevedono il metodo has()
per verificare se una chiave esiste e delete()
per eliminare un'associazione. Il metodo size()
consente di ottenere il numero degli elementi della mappa mentre get()
consente di accedere al valore associato ad una chiave:
myMap.delete(3.14);
console.log(myMap.has(3.14)); //visualizza false
console.log(myMap.size()); //visualizza 2
console.log(myMap.get("nome")); //visualizza 'Mario'
WeakMap
Una versione particolare di mappa associativa è rappresentata dalla WeakMap. Si tratta di una mappa simile alla Map
che abbiamo appena visto, ma i cui elementi sono gestiti tramite un riferimento debole
che non impedisce al garbage collector
di eliminarli dalla memoria.
In pratica è possibile che un elemento di una WeakMap
non sia più presente nella mappa senza una eliminazione esplicita da parte del codice, ma soltanto perché, non essendoci altri riferimenti attivi, il garbage collector
lo ha eliminato. È evidente che questo tipo di mappa ha applicazioni del tutto particolari. Ad esempio può essere utilizzata mettendo in corrispondenza elementi del DOM con dei valori. Se in seguito a manipolazioni del DOM un elemento viene eliminato, la corrispondente associazione nella WeakMap
scompare di conseguenza.
È da sottolineare che le WeakMap non ammettono chiavi di tipo semplice, come stringhe o numeri, ma soltanto oggetti.
4. Classi e oggetti
Diversi framework JavaScript hanno provato ad introdurre una qualche forma di programmazione orientata agli oggetti: da Prototype a MooTools, da Dojo a qooxdoo, per citarne qualcuno. Con l'avvento di Harmony avremo la possibilità di dichiarare le nostre classi in maniera nativa, come nel seguente esempio:
class Animale {
constructor(nome) {
this.nome = nome;
}
mangia(cibo) {
...
}
}
In esso dichiariamo la classe Animale
inizializzandola tramite il costruttore constructor() e definendo il metodo mangia()
. Utilizzeremo questa classe nel seguente modo:
var leone = new Animale("leone");
var gazzella = new Animale("gazzella");
leone.mangia(gazzella);
Possiamo però essere più specifici definendo una classe tramite ereditarietà, come mostrato di seguito:
class Leone extends Animale {
constructor(nome) {
super(nome);
this.numeroZampe = 4;
}
ruggisce() {
...
}
}
Come possiamo vedere, la parola chiave extends ci permette di derivare la classe Leone
dalla classe Animale
ereditandone le proprietà ed i metodi ed aggiungendone di propri. Tramite super() possiamo accedere alla classe base all'interno del costruttore o di qualsiasi metodo.
È prevista anche la possibilità di creare proprietà setter e getter tramite le parole chiave get
e set
. Il seguente esempio definisce un getter per definire il numero di zampe di un leone:
class Leone extends Animale {
constructor(nome) {
super(nome);
}
...
get numeroZampe() {
return 4;
}
}
C'è da segnalare tuttavia che, nonostante le apparenze, quello che è stato introdotto con le classi non aggiunge nulla di nuovo rispetto alla prassi consolidata in JavaScript di creare oggetti basati su funzioni. Infatti class è solo una semplificazione sintattica per continuare a creare oggetti nel solito modo. Non si tratta dunque dell'introduzione di veri e propri costrutti di programmazione orientata agli oggetti, ma di semplificazioni sintattiche che acquisicono e rendono ufficiali pattern di programmazione ormai consolidati.
Un'interessante novità riguarda la possibilità di definire operazioni da eseguire quando vengono effettuate modifiche su un oggetto. Possiamo infatti sfruttare Object.observe() per ricevere notifiche su aggiunte, modifiche e rimozioni di proprietà di un oggetto. Grazie a questa funzionalità possiamo realizzare meccanismi di data-binding senza bisogno di ricorrere a librerie esterne.
Il seguente esempio mostra il caso di un campo calcolato:
var persona = {nome: "Mario", cognome: "Rossi"};
var messaggio = "Ciao " + persona.nome;
Object.observe(persona,
function(changes) {
changes.forEach(function(change) {
if (change.type == "updated") && (change.name == "nome"){
messaggio = "Ciao " + change.object[change.name];
}
});
}
);
Abbiamo l'oggetto persona
la cui proprietà nome
viene utilizzata per comporre un messaggio di benvenuto. Mettiamo l'oggetto persona sotto osservazione tramite Object.observe()
definendo la funzione da eseguire quando si verificano aggiornamenti all'oggetto. La funzione riceve l'elenco delle modifiche e per ciascuna di esse esegue un'ulteriore funzione. Quest'ultima funzione intercetta la modifica (change.type == "updated"
) alla proprietà nome (change.name == "nome"
) e riformula il messaggio utilizzando il nuovo valore (change.object[change.name]
).
Pertanto l'assegnamento di un nuovo valore alla proprietà nome produrrà automaticamente l'aggiornamento del messaggio.
5. Organizzazione del codice
Proseguendo nella esplorazione delle caratteristiche della nuova versione di ECMAScript troviamo il supporto dei moduli per una migliore organizzazione del codice.
Moduli
Un modulo è una sezione di codice contenuta in un'apposita dichiarazione o in un file esterno. Per dichiarare un modulo inline, cioè all'interno di un blocco di codice più esteso, come ad esempio all'interno del file in cui verrà utilizzato, utilizzeremo la parola chiave module come mostrato di seguito:
module "Crittografia" {
...
}
All'interno del modulo scriveremo le nostre funzioni che possiamo rendere pubbliche, cioè accessibili dall'esterno del modulo, dichiarandole come export:
module "Crittografia" {
export function cripta(value) {
...
}
export function decripta(value) {
...
}
function calcolaHash(value) {
...
}
}
Nell'esempio le funzioni cripta()
e decripta()
saranno accessibili fuori dal modulo, mentre calcolaHash()
rimane privata all'interno del modulo.
È possibile importare funzionalità da un modulo tramite la parola chiave import:
import "Crittografia" as Crypto
import decripta from "Crittografia"
import {cripta, decripta} from "Crittografia"
import {cripta:crypt, decripta:decrypt} from "Crittografia"
Nel primo caso vengono importate tutte le funzioni esportate dal modulo Crittografia
ed il modulo stesso viene assegnato alla variabile Crypto
. In questo modo le funzioni del modulo possono essere invocate come Crypto.decripta()
e Crypto.cripta()
. Nel secondo caso viene importata la sola funzione decripta()
dal modulo Crittografia
mentre nel terzo caso vengono importate entrambe le funzioni. Nel terzo caso le due funzioni vengono importate rinominandole in crypt()
e decrypt()
. Quest'ultima caratteristica può risultare utile per evitare conflitti di nomi tra moduli diversi. È comunque da tenere presente che l'accesso ai moduli è in sola lettura, cioè non può essere modificato il contenuto di un modulo mediante codice contenuto in un altro modulo.
Moduli esterni
Abbiamo detto che un modulo può essere definito in un file esterno. In questo caso possiamo importarlo specificando il relativo URL:
import "http://miodominio.it/crittografia.js" as Crypto
Crypto.decripta("a19djet5Rhsdj?_is9")
È possibile inoltre caricare dinamicamente un modulo in un contesto controllato tramite un loader, come nel seguente esempio:
Loader.load("http://miodominio.it/crittografia.js",
function () {
//callback
},
function () {
//error callback
})
Il metodo load() prevede l'URL del modulo, una funzione di callback che viene eseguita in seguito al caricamento ed un'altra funzione di callback invocata in caso di errore. Segnaliamo tuttavia che grazie ad un insieme di API specifiche è possibile controllare il caricamento di un modulo in modo molto più avanzato di quanto presentato nel nostro esempio.
5. Conclusioni
Le funzionalità illustrate in questo articolo sono soltanto una parte delle novità introdotte dallo standard ECMAScript Harmony e che saranno disponibili con le prossime versioni di JavaScript.
Altre funzionalità sono in corso di definizione come ad esempio una sintassi sintetica per la definizione di funzioni (la cosiddetta fat arrow syntax), alcuni nuovi metodi relativi agli array o il supporto di Proxy. Naturalmente dato che lo standard non è definitivo le caratteristiche esposte potrebbero subire variazioni in alcuni dettagli, ma le linee guida sono ormai tracciate ed è opportuno cominciare a prendervi confidenza.
Ma allo stato attuale è possibile fare qualche esperimento? Esistono browser o tool che già supportano queste novità?
Effettivamente qualcosa è possibile fare, anche se naturalmente quello che è al momento disponibile è utilizzabile soltanto in via sperimentale. Infatti, dal momento che le specifiche sono ancora in corso di definizione, le implementazioni possono essere incomplete o difformi dallo standard.
Un'utile strumento di riferimento per avere un quadro dell'attuale supporto dello standard da parte dei più comuni browser e framework JavaScript è la tabella di compatibilità di Juriy Zaytsev, uno degli sviluppatori di Prototype, da cui risulta chiaro come la sua implementazione sia ancora molto lacunosa.
Tuttavia, per fare qualche esperimento concreto occorre utilizzare le versioni di sviluppo di alcuni browser, come ad esempio Firefox Aurora o Chrome Canary. Analogamente in node.js, che utilizza V8 il motore JavaScript di Chrome, è possibile attivare selettivamente le nuove funzionalità di ECMAScript specificando una serie di opzioni --harmony.
Array
A proposito di array, un'interessante semplificazione sintattica è la cosiddetta destrutturazione dell'assegnamento, cioè la possibilità di assegnare valori ad un array utilizzando direttamente la sua struttura sintattica. Analizziamo il seguente esempio di codice:
var [antipasto, primo, secondo, dessert] = ["Affettati vari", "Spaghetti alle vongole", "Brasato di vitello", "Tiramisù"];
console.log("Per secondo abbiamo " + secondo);
Abbiamo creato un array di variabili inizializzandolo in un unico assegnamento e poi abbiamo utilizzato una delle variabili appena definite. Il costrutto risulta molto sintetico e comodo. Inoltre la destrutturazione ci consente di ottenere in maniera concisa più valori da una funzione, come mostrato di seguito:
var [antipasto, primo, secondo, dessert] = getMenu();
Possiamo anche ignorare gli elementi dell'array che non ci interessano lasciando vuote le corrispondenti posizioni:
var [, primo, , dessert] = getMenu();
La destrutturazione può essere applicata anche agli oggetti, come possiamo vedere di seguito:
var { antipasto: a,
primo: p,
secondo: s,
dessert: d } = { antipasto: "Affettati vari",
primo: "Spaghetti alle vongole",
secondo: "Brasato di vitello",
dessert: "Tiramisù"};
console.log("Per secondo abbiamo " + s);
Ed anche in questo caso possiamo ignorare le proprietà a cui non siamo interessati:
var { primo: p, dessert: d } = getMenu();
quasi-literal/template strings
Altra funzionalità comoda per certi versi ma controversa per altri è l'introduzione dei quasi-literal (poi ribattezzati "template strings"), un costrutto sintattico per la gestione di template di stringhe. Vediamo un semplice esempio:
var text = `Questa è una stringa
su più righe.`;
In questa dichiarazione alla variabile text viene assegnata una stringa racchiusa tra i caratteri backtick (`
) da non confondere con l'apice singolo ('
). Questa stringa rappresenta un semplice esempio di quasi-literal, la cui differenza rispetto ad una normale stringa consiste nel fatto di mantenere i ritorni a capo e di non interpretare caratteri speciali come ad esempio n
. Già da questo semplice esempio emerge quella che si prospetta una difficoltà del suo impiego utilizzando tastiere italiane.
I quasi-literal o template strings prevedono la possibilità di utilizzare dei segnaposto al loro interno per creare dei veri e propri template, come mostrato nel seguente esempio:
var importo = 1000;
var msg = `L'importo è di $(importo) euro ($(importo + importo*21/100) euro IVA compresa)`;
console.log(msg);
Il risultato è rappresentato dal seguente testo:
L'importo è di 1000 euro (1210 euro IVA compresa)
Come è facile intuire, con $(importo)
abbiamo definito un segnaposto per la variabile importo
che viene valutata all'interno del quasi-literal. Oltre ad accettare variabili l'operatore $() accetta anche espressioni come quella che abbiamo utilizzato per calcolare l'importo comprensivo dell'IVA.
Ma i quasi-literal permettono di andare oltre queste funzionalità consentendo l'uso di handler per elaborazioni speciali e personalizzate, come mostrato nell'esempio che segue:
var quantita = 5;
var messaggio = elabora`Sono rimaste $(quantita) mele`;
In questo caso l'espressione racchiusa tra backtick è preceduta dal nome di un handler o tag che non è altro che una funzione il cui compito è di effettuare elaborazioni aggiuntive al risultato della valutazione del quasi-literal.
Alla funzione viene passato un primo parametro rappresentato da un array delle parti che costituiscono il quasi-literal seguito da tanti parametri quanti sono i segnaposto in esso contenuti. Ad esempio, alla funzione elabora()
verranno passati l'array ["Sono rimaste", "mele"]
come primo parametro ed il valore 5
come secondo parametro.
Un esempio di elaborazione aggiuntiva compiuta dalla funzione elabora()
può essere quella mostrata nel seguente esempio:
function elabora(literal, value) {
var result;
if (value == 1) {
result = "È rimasta " + value + " mela";
} else {
result = literal[0] + " " + value + " " + literal[1];
}
return result;
}
In questo caso l'handler riscrive la valutazione del quasi-literal adeguandola al singolare nel caso in cui il valore sia uguale a 1.
Le specifiche prevedono alcuni handler predefiniti, come msg
per la localizzazione delle stringhe e safehtml
per l'HTML escaping, ma allo stato attuale la loro definizione non è del tutto completa.
Un'altra novità riguarda la possibilità di definire parametri di default per le funzioni, così ad esempio la seguente definizione consente di impostare un valore predefinito per i parametri x
e y
nel caso in cui questi non vengano passati nella chiamata:
function somma(x = 0, y = 0) {
return x+y;
}
Questa semplice impostazione ci consente di evitare il controllo dell'esistenza di un valore per ciascun parametro passato.
2. Parametri di funzione
ECMAScript 6 introduce alcune caratteristiche che riguardano i parametri delle funzioni. Tra queste abbiamo la possibilità di definire parametri di default, così ad esempio la seguente definizione consente di impostare un valore predefinito per i parametri x
e y
nel caso in cui questi non vengano passati nella chiamata:
function somma(x = 0, y = 0) {
return x+y;
}
Questa semplice impostazione ci consente di evitare il controllo dell'esistenza di un valore per ciascun parametro passato.
È inoltre prevista la possibilità di avere funzioni con un numero indefinito di parametri. Ad esempio, la seguente funzione restituisce la somma di un numero indefinito di valori numerici:
function somma(...x) {
var risultato = 0;
for (let value of x) {
risultato = risultato + value
}
return risultato;
}
console.log(somma(23, 34));
console.log(somma(18, 41, 51, 3, 12));
La sintassi ...x
indica un numero indefinito di parametri, o rest parameters, facendoli confluire in un array x
di valori. Naturalmente una funzione può prevedere sia un numero di parametri fisso che un numero variabile. Ad esempio è possibile prevedere una funzione che sommi almeno due numeri:
function somma(a, b, ...x) {
var risultato = a + b;
for (let value of x) {
risultato = risultato + value
}
return risultato;
}
L'altra faccia della medaglia dei rest parameters è l'operatore spread che consente di distribuire gli elementi contenuti in un array all'elenco di parametri di una funzione:
var valori = [14, 33, 1, 62, 14];
risultato = somma(...valori);
In questo caso il significato dei tre puntini di sospensione è di assegnare ciascun elemento dell'array al corrispondente parametro della funzione.