Abbiamo visto le caratteristiche principali dei simboli ed abbiamo visto come è possibile identificarli utilizzando delle descrizioni. Ma che utilizzo concreto possiamo farne nelle nostre applicazioni JavaScript? Proviamo a suggerire i possibili utilizzi con qualche esempio.
Supponiamo di voler identificare un oggetto in maniera univoca assegnando un valore alla sua proprietà id:
var item = { descrizione: "Prodotto A", prezzo: 24.5 };
item.id = nuovoId();
L'approccio classico consiste nel prevedere una funzione, nuovoId()
nel nostro esempio, che gestisca un contatore globale da incrementare ad ogni nuova richiesta per un nuovo identificatore. Con i simboli possiamo evitare il ricorso a questa funzione scrivendo semplicemente:
item.id = Symbol();
È il runtime di JavaScript a generare per noi un valore univoco.
Un altro possibile utilizzo è nella definizione di enumerazioni. Dal momento che JavaScript non supporta nativamente le enumerazioni, un approccio molto diffuso prevede di definire delle variabili come nel seguente esempio:
var giorniDellaSettimana = {
lunedi: 1,
martedi: 2,
mercoledi: 3,
giovedi: 4,
venerdi: 5,
sabato: 6,
domenica: 7
};
Un problema con questo approccio è che eventuali funzioni che utilizzano i giorni della settimana definiti in questo modo stanno lavorando in realtà con valori interi e potrebbero essere utilizzati male, volontariamente o involontariamente. Immaginiamo la seguente funzione:
function isGiornoLavorativo(giorno) {
return (giorno != giorniDellaSettimana.sabato && giorno != giorniDellaSettimana.domenica);
}
console.log(isGiornoLavorativo(giorniDellaSettimana.lunedi)); //true
console.log(isGiornoLavorativo(giorniDellaSettimana.sabato)); //false
Essa restituisce true
o false
in base al fatto che il valore dell'enumerazione rappresenta un giorno lavorativo o meno. Nulla vieta però di passare alla funzione direttamente un numero o il risultato di una somma:
console.log(isGiornoLavorativo(3)); // true
console.log(isGiornoLavorativo(2+5)); //false
Questo crea non poca confusione. Possiamo evitare questo definendo l'enumerazione tramite simboli:
var giorniDellaSettimana = {
lunedi: Symbol(),
martedi: Symbol(),
mercoledi: Symbol(),
giovedi: Symbol(),
venerdi: Symbol(),
sabato: Symbol(),
domenica: Symbol()
};
Questo fa sì che a ciascun elemento dell'enumerazione venga assegnato un valore univoco ed è solo quel valore che rappresenta quello specifico giorno della settimana. Siamo "costretti" quindi ad utilizzare esclusivamente gli elementi dell'enumerazione per fare riferimento ai giorni della settimana.
Un altro utilizzo è rappresentato dalla possibilità di definire proprietà per un oggetto senza il rischio di collisione di nomi. Supponiamo di avere necessità di aggiungere a runtime una proprietà ad un oggetto esistente, ad esempio per assegnare un valore che riutilizzeremo in un altro punto dell'applicazione:
var item = new Item();
item.dataControllo = new Date();
In questo esempio creiamo la proprietà dataControllo
e le assegniamo la data e ora corrente. Ma siamo sicuri che l'oggetto item
non avesse già la proprietà dataControllo
? E se anche non l'avesse, come facciamo a garantirci che non ci siano conflitti di nome in eventuali future evoluzioni della struttura dell'oggetto item
? Cioè, se la definizione della struttura dell'oggetto item
non è sotto il nostro controllo, come possiamo assicurarci che in futuro non venga aggiunta una proprietà dataControllo
?
In queste situazioni è meglio ricorrere ai simboli:
var item = new Item();
var dataControllo = Symbol();
item[dataControllo] = new Date();
In questo modo abbiamo la garanzia di non incorrere in conflitti di nomi nell'aggiunta di una nuova proprietà all'oggetto. Tra l'altro, la nuova proprietà non sarà visibile nell'elenco delle proprietà dell'oggetto accessibile tramite Object.getOwnPropertyNames(). Questo ci garantisce che eventuali routine di metaprogrammazione sulle proprietà continuino a funzionare correttamente.
Possiamo comunque accedere alle proprieta definite tramite simboli sfruttando il metodo Object.getOwnPropertySymbols():
var props = Object.getOwnPropertySymbols(item);
console.log(props.length); // risultato: 1
console.log(item[props[0]]); // risultato: valore della data assegnata
Un altro interessante utilizzo è quello del simbolo predefinito Symbol.iterator. Questo simbolo permette di rendere iterabile una collezione di valori, cioè è possibile esplorare la collezione di valori tramite un ciclo for...of. Proviamo a spiegare il concetto con un esempio. Supponiamo di avere il seguente costruttore che utilizziamo per memorizzare un elenco indefinito di valori:
function Collection() {};
var collection = new Collection();
collection[0] = 123;
collection[1] = "test";
collection[2] = 222;
Per elencare sulla console il contenuto della collezione dovremmo ricorrere al metodo Object.getOwnPropertyNames() per accedere alle proprietà dell'oggetto e visualizzarne il valore:
var props = Object.getOwnPropertyNames(collection);
for (var i = 1; i < props.length; i++ ) {
console.log(collection[props[i]]);
}
Possiamo semplificare la vita a chi utilizzerà la nostra collezione rendendola iterabile. Per fare ciò, dobbiamo definire un metodo nel costruttore che abbia come nome Symbol.iterator, come mostrato di seguito:
function Collection() {};
Collection.prototype[Symbol.iterator] = function() {
var self = this;
var i = 0;
return {
next: function() {
if (self[i] !== undefined) {
return { value: self[i++] };
} else {
return { done: true };
}
}
}
}
Come possiamo vedere, il nostro metodo restituisce un oggetto con un metodo next(). Questo metodo permette di restituire l'elemento successivo nella collezione. Nel nostro caso specifico, esso restituisce la proprietà successiva identificata dall'indice i
. Il metodo next()
restituisce a sua volta un oggetto con la proprietà value
a cui è assegnato il valore della proprieta corrente. Quando non ci sono più valori disponibili viene restituito un oggetto con una proprietà done
impostata a true
. Questo indica che l'iterazione è terminata.
Con l'implementazione di questo metodo, gli utilizzatori della nostra collezione potranno semplicemente utilizzare for...of per accedere al suo contenuto:
for (var x of collection) {
console.log(x);
}
In conclusione, questo nuovo tipo di dato è a prima vista un po' incomprensibile, ma una volta che abbiamo individuato le sue caratteristiche di base possiamo sfruttarlo per implementare funzionalità molto interessanti.