Nella programmazione orientata agli oggetti una classe è un costrutto di un linguaggio di programmazione usato come modello per creare oggetti. Il modello comprende attributi e metodi che saranno condivisi da tutti gli oggetti creati (istanze) a partire dalla classe. Un “oggetto” è, di fatto, l’istanza di una classe.
In pratica, spesso abbiamo bisogno di creare più oggetti dello stesso tipo, come utenti, beni o altro.
Come già sappiamo dal capitolo Costruttore, operatore "new", new function
ci può aiutare in questo.
Ma nel JavaScript moderno c’è un costrutto “class” più avanzato, che introduce nuove possibilità molto utili per la programmazione ad oggetti.
La sintassi di “class”
La sintassi base è:
class MyClass {
// metodi della classe
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
new MyClass()
creerà un nuovo oggetto con tutti i metodi presenti nella classe.
Il metodo constructor()
viene chiamato automaticamente da new
, dunque possiamo usarlo per inizializzare l’oggetto.
Per esempio:
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}
// Utilizzo:
let user = new User("John");
user.sayHi();
Quando viene chiamato new User("John")
:
- Viene creato un nuovo oggetto;
- Il metodo
constructor()
viene richiamato e assegna athis.name
l’argomento dato.
…Ora possiamo chiamare i metodi, per esempio user.sayHi
.
Un errore comune per i principianti è separare i metodi con delle virgole, portando ad un syntax error.
La notazione delle classi non va confusa con la notazione letterale per gli oggetti. In una classe non sono richieste virgole.
Cos’è una classe?
Dunque, cos’è esattamente una class
? A differenza di ciò che si potrebbe pensare, non si tratta di un concetto completamente nuovo.
Vediamo quindi cos’è effettivamente una classe. Questo ci aiuterà a comprendere aspetti più complessi.
In JavaScript, una classe è una specie di funzione.
Osserva:
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// prova: User è una funzione
alert(typeof User); // function
Il costrutto class User {...}
dunque:
- Crea una funzione chiamata
User
, che diventa il risultato della dichiarazione della classe. Le istruzioni della funzione provengono dal metodoconstructor
(considerato vuoto se non presente); - Salva tutti i metodi (come
sayHi
) all’interno diUser.prototype
.
Quando richiameremo da un oggetto un metodo, questo verrà preso dal prototipo (prototype), come descritto nel capitolo F.prototype. Dunque un oggetto new User
ha accesso ai metodi della classe.
Possiamo rappresentare il risultato della dichiarazione di class User
come:
Il codice seguente ti permetterà di analizzarlo:
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// una classe è una funzione
alert(typeof User); // function
// ...o, più precisamente, il costruttore
alert(User === User.prototype.constructor); // true
// I metodi sono in User.prototype:
alert(User.prototype.sayHi); // il codice del metodo sayHi
// ci sono due funzioni all'interno del prototipo
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
Non solo una semplificazione (syntax sugar)
Talvolta si pensa che class
in JavaScript sia solo “syntax sugar” (una sintassi creata per semplificare la lettura, ma che non apporta nulla di nuovo), dato che potremmo potremmo dichiarare la stessa cosa senza utilizzare la parola chiave class
:
// la classe User usando solo funzioni
// 1. Costruttore
function User(name) {
this.name = name;
}
// tutte le funzioni hanno un costruttore predefinito (di default)
// dunque non va creato
// 2. Aggiungiamo un metodo al prototipo
User.prototype.sayHi = function() {
alert(this.name);
};
// Utilizzo:
let user = new User("John");
user.sayHi();
Il risultato di questo codice è circa lo stesso. È quindi logico pensare che class
sia solo una semplificazione sintattica (syntax sugar).
Ci sono però delle importanti differenze.
-
Una funzione creata attraverso
class
viene etichettata dalla speciale proprietà interna[[IsClassConstructor]]: true
. Quindi non è esattamente uguale che crearla manualmente.A differenza di una normale funzione, il costruttore di una classe può essere richiamato solo attraverso la parola chiave
new
:class User { constructor() {} } alert(typeof User); // funzione User(); // Errore: Il costruttore della classe può essere richiamato solo attraverso 'new'
Inoltre, nella maggior parte dei motori JavaScript il costruttore comincia con “class”
class User { constructor() {} } alert(User); // class User { ... }
Ci sono altre differenze, che scopriremo più avanti.
-
I metodi delle classi non sono numerabili. La definizione di una classe imposta il flag
enumerable
afalse
per tutti i metodi all’interno di"prototype"
.Questo è un bene, dato che non vogliamo visualizzare i metodi quando utilizziamo un ciclo
for..in
per visualizzare un oggetto. -
Il contenuto di una classe viene sempre eseguito in
strict
.Oltre a queste, la sintassi
class
apporta altre caratteristiche, che esploreremo più avanti.
L’espressione class
Come le funzioni, le classi possono essere definite all’interno di un’altra espressione, passata come parametro, essere ritornata (returned), assegnata (assigned) ecc.
Qui c’è un piccolo esempio:
let User = class {
sayHi() {
alert("Hello");
}
};
In maniera simile alle funzione nominate (Named Function Expression), le classi possono avere o meno un nome.
Se una classe ha un nome, esso è visibile solo all’interno della classe:
// "Named Class Expression"
// (la classe non ha un nome)
let User = class MyClass {
sayHi() {
alert(MyClass); // MyClass è visibile solo all'interno della classe
}
};
new User().sayHi(); // funziona, restituisce la definizione di MyClass
alert(MyClass); // errore, MyClass non è visibile al di fuori della classe
Possiamo anche creare delle classi “on-demand”:
function makeClass(phrase) {
// dichiara una classe e la restituisce
return class {
sayHi() {
alert(phrase);
}
};
}
// Crea una nuova classe
let User = makeClass("Hello");
new User().sayHi(); // Hello
Getters/setters e altre scorciatoie
Così come negli oggetti letterali (literal objects), le classi possono includere getters/setters, generatori, proprietà eccetera.
L’esempio seguente implementa user.name
attraverso get/set
:
class User {
constructor(name) {
// invoca il setter
this.name = name;
}
get name() {
return this._name;
}
set name(value) {
if (value.length < 4) {
alert("Name is too short.");
return;
}
this._name = value;
}
}
let user = new User("John");
alert(user.name); // John
user = new User(""); // Nome troppo corto.
La dichiarazione della classe crea i getter e i setter all’interno di User.prototype
:
Computed names […]
A seguire un esempio con le proprietà:
function f() { return "sayHi"; }
class User {
[f()]() {
alert("Hello");
}
}
new User().sayHi();
Per creare un metodo generatore è sufficiente aggiungere *
prima del nome della funzione.
Proprietà di una classe
Le proprietà di una classe dichiarata in questo modo sono una novità del linguaggio.
Negli esempi riportati sopra, la classe User
conteneva solo dei metodi. Aggiungiamo una proprietà:
class User {
name = "Anonymous";
sayHi() {
alert(`Hello, ${this.name}!`);
}
}
new User().sayHi(); // Hello, John!
Quindi scriviamo semplicemente “
La differenza importante dei campi di una classe è che vengono impostati sull’oggetto individuale e non su User.prototype
:
class User {
name = "John";
}
let user = new User();
alert(user.name); // John
alert(User.prototype.name); // undefined
Possiamo anche assegnare valori utilizzando espressioni più complesse e chiamate a funzioni:
class User {
name = prompt("Name, please?", "John");
}
let user = new User();
alert(user.name); // John
Creazione di metodi vincolati a campi di classe
Come dimostrato nel capitolo Function binding, le funzioni in JavaScript hanno un this
dinamico che dipende dal contesto della chiamata.
Quindi, se un metodo di un object viene passato e chiamato in un altro contesto, this
non sarà più un riferimento al suo object.
Per esempio, questo codice mostrerà undefined
:
class Button {
constructor(value) {
this.value = value;
}
click() {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // undefined
Il problema viene chiamato “perdita del this
”.
Ci sono due differenti approcci per affrontare questo problema, come discusso nel capitolo Function binding:
- Passare una funzione contenitore, come
setTimeout(() => button.click(), 1000)
. - Associare il metodo all’oggetto, e.g. nel costruttore.
I campi di una classe forniscono un’altra sintassi molto più elegante:
class Button {
constructor(value) {
this.value = value;
}
click = () => {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // hello
Il campo della classe click = () => {...}
viene creato per ogni oggetto, abbiamo quindi una funzione diversa per ogni Button
, con il riferimento this
che punta all’oggetto. Possiamo passare button.click
ovunque, e il valore di this
sarà sempre quello corretto.
Questo è particolarmente utile in ambiente browser, per gli event listeners (ascoltatori di eventi).
Riepilogo
Il seguente esempio riporta la sintassi base di una classe:
class MyClass {
prop = value; // proprietà
constructor(...) { // costruttore
// ...
}
method(...) {} // metodo
get something(...) {} // metodo getter
set something(...) {} // metodo setter
[Symbol.iterator]() {} // metodo creato con un vettore relazionale
// ...
}
MyClass
è tecnicamente una funzione (che corrisponde a constructor
), mentre i metodi vengono scritti in MyClass.prototype
.
Nei prossimi capitoli impareremo altri dettagli riguardo alle classi, come l’ereditarietà.