Oggetti in JavaScript
Le variabili di tipo oggetto sono collezioni arbitrarie di proprietà che possiamo aggiungere o
eliminare a piacere.
Abbiamo già avuto modo di utilizzare degli oggetti. Sono oggetti, ad esempio, le stringhe e gli array.
Gli oggetti, come già visto per altri linguaggi di programmazione, oltre alle proprietà, sono dotati di metodi, che sono funzioni in grado di modificare lo stato dell’oggetto e di ritornare (eventualmente) un valore.
Nel caso degli array è stata messa in evidenza la proprietà length,
che è una proprietà perchè contiene il numero di elementi dell’array stesso.
La funzione push() che permette di aggiungere un
elemento all’array è invece da considerarsi un metodo, perchè si manifesta
come una funzionalità.
In JavaScript, quasi tutto è un oggetto e quello che non lo è può diventarlo a breve giro; nel caso seguente definiamo un oggetto p (prodotto) dotato di due attributi ( le proprietà) stampando a video il valore di una di queste
var p={prd:'dado', prx:2};
console.log(p.prd);
//____>dado
Dunque, per creare un oggetto si può usare una notazione a parentesi graffe.
Dentro le parentesi graffe possiamo specificare elenchi di proprietà separati da virgole.
Ciascuna proprietà è rappresentata col suo nome, seguito da un segno di due punti e da
un'espressione che le assegna un valore.
Si vede che per riferirsi ad una data proprietà di un oggetto basta rispettare la sintassi
nomeOggetto.nomeProprietà
Il frammento di codice precedente, evidenzia una certa differenza rispetto ad altri linguaggi di programmazione dove la creazione e la gestione degli oggetti impone dei rigorosi passaggi formali che non possono essere evitati.
Negli altri linguaggi, ci si basa sul paradigma classe-istanza dove la classe è una entità astratta che definisce le proprietà ed il comportamento di un dato oggetto, mentre l’istanza è l’implementazione della classe in un oggetto concreto.
Nell'esempio visto, per creare un oggetto abbiamo impiegato due righe; la stessa procedura eseguita in Java non prevede esattamente due righe:
class main {
public static void main (String[] args){
P p=new P("dado",1);//istanziazione
System.out.println(p.prd);
}//fine main
class P{
private String prd;
private int prx;
P(String pro,int pre){//costruttore
prd=pro;
prx=pre;
}//fine costruttore
}//fine classe P
L’oggetto istanziato dall’istruzione
P p=new P("dado",1);
si basa sulla classe P definita successivamente.
In JavaScript la nozione di classe non esiste; al suo posto è stato introdotto
il concetto di prototipo.
Un prototipo è un altro oggetto che viene usato come "sorgente" di proprietà.
Quando un oggetto riceve una richiesta per una proprietà che non ha, la
proprietà viene cercata nel suo prototipo; se non la si trova nemmeno lì,
la ricerca continua nel prototipo del prototipo e così via.
Tutte le funzioni derivano da Function.prototype
e tutti gli array da Array.prototype; il prototipo
di tutti i prototipi è per definizione Object.prototype il quale mantiene
attributi e metodi generali e comuni a tutti gli oggetti come ad esempio
il metodo toString() usato per rivelare lo stato
di un oggetto.
In questo modo, si crea automaticamente una gerarchia di prototipi; così
come accade in un linguaggio basato su classi, dove si crea una gerarchia
tra le classi attraverso le definizioni delle classi stesse.
Questo approccio alla programmazione ad oggetti risulta essere molto flessibile
tanto che una volta creato un oggetto risulta facile aggiungere attributi
(proprietà).
var p={prd:'dado', prx:1};
p.qta=24;
console.log(p.qta);
//____>24
basta attenersi alla sintassi
nomeOggetto.nuovoAttributo=valore;
E' facile anche eliminare una proprietà da un oggetto; tramite l'operatore unario delete :
delete nomeOggetto.nomeAttributo;
var p={prd:'dado', prx:1};
p.qta=24;
console.log(p.qta);
delete p.qta;
console.log(p.qta);
//____>24
//____>undefined
Prototype
Non sarebbe corretto affermare che in JavaScript la classe sia stata completamente abolita: è rimasto soltanto il costruttore che, come in Java, coincide con una funzione
function pro(x,y,z) {
this.prd = x;
this.prx = y;
this.qta = z;
this.print = function(){return this.prd + ' ' +
this.prx};
}//fine prototipo
var v=new pro('vite',3,50)
var d=new pro('dado',2,75)
console.log(p.print());
//________>vite 3
In questo caso, vediamo in azione un costruttore completo, costituito dai
tre attributi prd (nome del prodotto) prx
(prezzo unitario) qta (quantità in magazzino).
Si vede anche il metodo print() che restituisce il
nome del prodotto con il suo prezzo.
Nel costruttore, la variabile this è legata al nuovo oggetto che si sta
creando e che verrà restituito dalla chiamata se non è previsto esplicitamente
un diverso oggetto come valore di restituzione.
In Javascript quando si invoca una funzione con la parola chiave new la
si tratta come un costruttore.
I costruttori, come tutte le funzioni dispongono automaticamente di una
proprietà, chiamata prototype che contiene un oggetto predefinito e vuoto
ottenuto da Object.prototype.
Dunque per aggiungere un metodo valore() agli oggetti
di tipo pro sarà sufficiente scrivere
pro.prototype.valore = function(){return this.qta*this.prx};
console.log(d.valore());
//________>vite 3
//________>150
il metodo valore restituisce il controvalore in magazzino di un determinato prodotto; applicato all'oggetto d (dado) deve restituire 150, perchè ci sono 75 dadi che costano 2€ ciascuno.
Quando ad un oggetto si aggiunge una proprietà, non importa se presente nel prototipo o meno, la proprietà viene aggiunta all'oggetto stesso. Se il prototipo dispone di una proprietà con lo stesso nome, questa non avrà effetto sull'oggetto ed il prototipo rimane invariato.
Incapsulamento
Le prerogative della filosofia dell'information hiding (incapsulamento)
possono essere rispettate dichiarando all'interno di un costruttore l'attributo
come
var nomeVar=nomeParametro;
invece che con
this.nomeVar=nomeParametro;
function pro(x,y,z) {
this.prd = x;
this.prx = y;
var qta = z;
this.print = function(){return this.prd + ' ' + qta};
}//fine prototipo
var v=new pro('vite',3,50)
console.log(v.prx);
console.log(v.qta);
console.log(v.print());
//__>3
//__>undefined
//__>vite 50
Si vede, in questo caso, come l'attributo qta sia
accessibile solo tramite un metodo print()
definito all'interno del costruttore.
Possiamo riassumere dicendo che a differenza di altri linguaggi dove classi
ed istanze sono entità separate, e l'istanziazione avviene rigorosamente
con un metodo costruttore.
Ereditarietà
L'aspetto dell'ereditarietà, che caratterizza la programmazione ad oggetti tradizionale, può essere applicato anche JS.
La tecnica più tradizionale della OOP consiste nel creare una classe base, dove sono definiti attributi e metodi comuni a tipi di oggetti diversi, e poi definire classi derivate che, oltre a definire attributi e metodi specifici, ereditano gli attributi e metodi della classe base; in JavaScript è sufficiente disporre una funzione costruttore per creare una collezione di oggetti ed ereditare le proprietà seguendo la catena dei prototipi.
Supponiamo di dover descrivere una realtà lavorativa dove esistono operatori che possono avere parametri comuni ma anche diversificati come descritto nel seguente schema:
L'entità principale, denominata Soggetto è caratterizzata
dai due attributi (nome,eta) comuni a tutte le successive
istanze che saranno create. L'istanza Tecnico eredita
questi due attributi e ne aggiunge un altro (paga).
L'istanza Operatore eredita i due attributi da Soggetto
e ne aggiunge altri due più specifici (macchina, paga).
L'istanza Responsabile può essere ottenuta da una
istanza Tecnico, da cui vengono ereditati in totale
tre attributi, ma di un responsabile può interessare l'esperienza, per cui
viene aggiunto un ulteriore attributo (servizio=anni
di servizio).
Per garantire l'estensione di una classe verso l'altra si utilizza il metodo apply() che ha lo stesso compito del metodo Super() utilizzato in Java (invocazione alla superclasse genitrice). Nei costruttori derivati : Operatore, Tecnico e Responsabile, si nota come il metodo info() debba essere riscritto ( o meglio sovrascritto=override) a causa dei parametri che si diversificano. Questa operazione può essere fatta nel modo indicato oppure esternamente alla funzione costruttrice ad esempio attraverso l'istruzione:
Responsabile.prototype.info = function(){
return this.nome+' '+this.eta+' '+this.paga+'€ '+this.servizio;
};
Ovviamente se il costruttore base contiene già attributi necessari al metodo non è necessario riscrivere quest'ultimo nella funzione costruttrice derivata.
cioè, un oggetto può essere usato come oggetto del proprio tipo o come oggetto del suo tipo base; quest'ultima operazione viene solitamente chiamata upcasting.
Polimorfismo
Per polimorfismo si intende la possibilità di dare lo stesso nome a procedure
che fanno cose diverse.
In ogni linguaggio esistono sorgenti di polimorfismo; ad esempio in JS quando
scriviamo
3+2
//otteniamo_____>5
mentre quando scriviamo
'conca'+'tena'
//otteniamo_____>'concatena'
Allora, con lo stesso simbolo + indichiamo operazioni
diverse: addizione se gli operandi sono numeri, concatenazione se gli operandi
sono stringhe.
Questo particolare tipo di polimorfismo si chiama overloading
(sovvracarico).
La decisione su quale tipo di operazione da intraprendere può essere presa
in anticipo sulla base del tipo di operandi; ma questa non è una novità,
gia sappiano che tutti i moderni linguaggi di programmazione permettono
la scrittura di molteplici funzioni che abbiano lo stesso nume ma segnatura
diversa (diverso numero di parametri o stesso numero di parametri ma di
diverso tipo).
In JS questo meccanismo viene spinto oltre; in virtù del fatto che una funzione
può accettare come parametro un'altra funzione, come si vede dall'esempio
che segue.
L'insieme delle nozioni di incapsulazione, polimorfismo ed ereditarietà
sono considerati delle componenti fondamentali della programmazione ad oggetti.
Mentre però i primi due concetti vengono generalmente considerati molto
utili, l'ereditarietà è spesso soggetta a delle critiche.
Le ragioni principali sono che spesso viene confusa con il polimorfismo
ed è comunque sopravalutata ed usata , talvolta, a sproposito.
Mentre incapsulazione e polimorfismo sono spesso usati per disgiungere tra
loro parti di codice (che non devono potersi influenzare a vicenda) l'ereditarietà
lega tra loro tipi diversi creando più confusione.
Questo per dire che l'ereditarietà dovrebbe essere vista solo come una tecnica
per definire nuovi tipi di dato risparmiando righe di istruzioni e non come
un principio fondamentale di organizzazione del codice.
Nell'ultimo esempio, abbiamo comunque visto, come si possa avere comunque,
polimorfismo senza ereditarietà.
Array di oggetti
Come si può immaginare, è possibile usare un array per creare e gestire una collezione di oggetti, essendo l'array a sua volta un oggetto, non è nemmeno necessario dichiarare preventivamente un costruttore come si vede dal seguente esempio:
però, se si vuole proprio usare un costruttore, anche solo per un'ovvia ragione di ordine e leggibilità, è sempre possibile farlo basandosi, ad esempio, sul codice seguente.
Si riconosce una notevole analogia con gli array di oggetti del linguaggio Java. Si vede come sia possibile accedere direttamente agli attributi dell'oggetto oppure tramite metodi preposti (funzione print in questo caso) .