JavaScript include le funzionalità per la programmazione orientata agli oggetti (OOP). Nella OOP, vuoi creare oggetti (istanze) a partire da «modelli» (classi) in modo che includano determinati dati e funzionalità. Le proprietà che contengono dati si chiamano campi nel contesto della OOP, mentre le proprietà che contengono funzioni si chiamano metodi.
JavaScript non aveva affatto le classi prima che fossero aggiunte alla specifica del linguaggio nel 2015, ma permetteva la programmazione orientata agli oggetti tramite l'ereditarietà basata su prototipi.
E anche se oggi è disponibile la parola chiave class, JavaScript è ancora un linguaggio basato sui prototipi.
Per capire cosa significa essere un linguaggio basato sui prototipi e come funziona davvero JavaScript, torniamo indietro al periodo in cui le classi non esistevano.
In JavaScript, il modello (la classe) è realizzato tramite una funzione normale.
Quando una funzione deve essere usata come modello, si chiama funzione costruttrice e la convenzione è che il nome della funzione inizi con la lettera maiuscola.
Le istanze (gli oggetti) vengono create dal modello usando la parola chiave new quando si chiama la funzione costruttrice.
function Car() {
// ...
}
const myCar = new Car();
const yourCar = new Car();
È importante notare che in JavaScript le istanze e la funzione costruttrice mantengono una relazione reciproca anche dopo che le istanze sono state create.
Ogni oggetto istanza include una proprietà interna nascosta, indicata come [[prototype]] nella specifica del linguaggio.
Contiene un riferimento al valore della chiave prototype della funzione costruttrice.
Sì, hai letto bene: una funzione JavaScript può avere coppie chiave/valore perché, dietro le quinte, è anche un oggetto.
Per riassumere:
prototype del costruttore (della funzione) diventerà il prototipo dell'istanza.Spesso vuoi che tutti gli oggetti derivati (le istanze) includano alcuni campi e ricevano dei valori iniziali per quei campi quando l'oggetto viene costruito.
Tutto questo si può realizzare tramite la parola chiave this.
All'interno della funzione costruttrice, this rappresenta il nuovo oggetto che verrà creato tramite new.
this viene restituito automaticamente dalla funzione costruttrice quando viene chiamata con new.
Questo significa che possiamo aggiungere campi alla nuova istanza aggiungendoli a this nella funzione costruttrice.
function Car(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false
I metodi vengono aggiunti tramite la proprietà prototype della funzione costruttrice.
All'interno di un metodo, puoi accedere ai campi dell'istanza tramite this.
Questo funziona grazie alla seguente regola generale.
Quando una funzione viene chiamata come metodo di un oggetto, il suo
thisviene impostato sull'oggetto su cui il metodo è chiamato. 1
function Car() {
this.engineRunning = false;
// ...
}
Car.prototype.startEngine = function () {
this.engineRunning = true;
};
Car.prototype.addGas = function (litre) {
// ...
};
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
myCar nell'esempio qui sopra è un normale oggetto JavaScript e, se lo esaminassimo (ad esempio nella console del browser), non troveremmo una proprietà startEngine con una funzione come valore direttamente dentro l'oggetto myCar.
Quindi, come funziona il codice qui sopra?
Il segreto è quello che si chiama catena dei prototipi.
Quando provi ad accedere a una proprietà qualsiasi (campo o metodo) di un oggetto, JavaScript controlla prima se la chiave corrispondente si trova direttamente nell'oggetto stesso.
In caso contrario, continua a cercare la chiave nell'oggetto a cui fa riferimento la proprietà [[prototype]] dell'oggetto originale.
Come accennato prima, nel nostro esempio [[prototype]] punta alla proprietà prototype della funzione costruttrice.
È lì che JavaScript troverebbe la funzione startEngine, perché l'abbiamo aggiunta proprio lì.
function Car() {
// ...
}
Car.prototype.startEngine = function () {
// ...
};
E la catena non finisce qui.
La proprietà [[prototype]] di Car.prototype (myCar.[[prototype]].[[prototype]]) fa riferimento a Object.prototype (la proprietà prototype della funzione costruttrice Object).
Contiene metodi generali disponibili per tutti gli oggetti JavaScript, ad esempio toString().
In conclusione, puoi chiamare myCar.toString() e quel metodo esisterà perché JavaScript lo cerca lungo tutta la catena dei prototipi.
Nota che la catena dei prototipi viene percorsa solo quando si recupera un valore. Impostare direttamente una proprietà o eliminare una proprietà di un oggetto istanza riguarda solo quella specifica istanza. Potrebbe non essere quello che ti aspetti se sei abituato a un linguaggio con ereditarietà basata sulle classi.
Oggi JavaScript supporta la definizione di classi con la parola chiave class.
Questa è stata aggiunta alla specifica del linguaggio nel 2015.
Da un lato, questo fornisce dello zucchero sintattico che rende le classi più facili da leggere e scrivere.
La nuova sintassi è più simile a come si scrivono le classi in linguaggi come C++ o Java.
Gli sviluppatori che passano da quei linguaggi si adattano più facilmente.
Dall'altro lato, la sintassi delle classi apre la strada a nuove funzionalità del linguaggio che non sono disponibili nella sintassi dei prototipi.
Con la nuova sintassi, le classi si definiscono con la parola chiave class, seguita dal nome della classe e dal corpo della classe tra parentesi graffe.
Il corpo contiene la definizione della funzione costruttrice, cioè un metodo speciale con il nome constructor.
Questa funzione funziona esattamente come la funzione costruttrice nella sintassi dei prototipi.
Il corpo della classe contiene anche tutte le definizioni dei metodi.
La sintassi dei metodi è simile alla notazione abbreviata che abbiamo visto per aggiungere funzioni come valori dentro un oggetto, vedi Concetto Oggetti.
class Car {
constructor(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
startEngine() {
this.engineRunning = true;
}
addGas(litre) {
// ...
}
}
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
Tieni presente che, dietro le quinte, JavaScript è ancora un linguaggio basato sui prototipi. Tutti i meccanismi che abbiamo imparato nella sezione "Sintassi dei prototipi" qui sopra valgono ancora.
Per impostazione predefinita, in JavaScript tutti i campi delle istanze sono pubblici. È possibile accedervi e assegnarli direttamente.
Tuttavia, esiste una convenzione consolidata secondo cui i campi e i metodi che iniziano con un underscore vanno trattati come privati. Non dovrebbero mai essere accessibili direttamente dall'esterno della classe.
I campi privati sono a volte accompagnati da getter e setter.
Con le parole chiave get e set puoi definire funzioni che vengono eseguite quando si accede a una proprietà con lo stesso nome della funzione o le si assegna un valore.
class Car {
constructor() {
this._mileage = 0;
}
get mileage() {
return this._mileage;
}
set mileage(value) {
throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
// Just an example, usually you would not provide a setter in this case.
}
}
const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.
Esempi di this: come metodo di un oggetto, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (consultato il 3 dicembre 2021) ↩
In questo esercizio simulerai un sistema informatico basato su finestre. Creerai alcune finestre che possono essere spostate e ridimensionate. L'immagine seguente è rappresentativa dei valori con cui lavorerai qui sotto.
<--------------------- screenSize.width --------------------->
^ ┌────────────────────────────────────────────────────────────┐
| │ │
| │ position.x, _ │
| │ position.y \ │
| │ \<----- size.width -----> │
| │ ^ *──────────────────────┐ │
| │ | │ title │ │
| │ | ├──────────────────────┤ │
screenSize.height │ | │ │ │
| │ size.height │ │ │
| │ | │ contents │ │
| │ | │ │ │
| │ | │ │ │
| │ v └──────────────────────┘ │
| │ │
| │ │
v └────────────────────────────────────────────────────────────┘
📣 Per mettere in pratica la tua ampia gamma di competenze in JavaScript, prova a risolvere le attività 1 e 2 con la sintassi dei prototipi e le attività rimanenti con la sintassi delle classi.
Definisci una classe (funzione costruttrice) di nome Size.
Dovrebbe avere due campi, width e height, che memorizzano le dimensioni attuali della finestra.
La funzione costruttrice dovrebbe accettare dei valori iniziali per questi campi.
Il valore di width viene fornito come primo parametro, quello di height come secondo.
I valori predefiniti di width e height dovrebbero essere 80 e 60, rispettivamente.
Definisci inoltre un metodo resize(newWidth, newHeight) che accetta come parametri una nuova larghezza e una nuova altezza e modifica i campi in modo che riflettano le nuove dimensioni.
const size = new Size(1080, 764);
size.width;
// => 1080
size.height;
// => 764
size.resize(1920, 1080);
size.width;
// => 1920
size.height;
// => 1080
Definisci una classe (funzione costruttrice) di nome Position con due campi, x e y, che memorizzano rispettivamente la posizione orizzontale e quella verticale dell'angolo in alto a sinistra della finestra.
La funzione costruttrice dovrebbe accettare dei valori iniziali per questi campi.
Il valore di x viene fornito come primo parametro, quello di y come secondo.
Il valore predefinito dovrebbe essere 0 per entrambi i campi.
La posizione (0, 0) è l'angolo in alto a sinistra dello schermo: i valori di x aumentano man mano che ci si sposta verso destra e i valori di y aumentano man mano che ci si sposta verso il basso.
Definisci anche un metodo move(newX, newY) che accetta come parametri i nuovi valori di x e y e modifica le proprietà in modo che riflettano la nuova posizione.
const point = new Position();
point.x;
// => 0
point.y;
// => 0
point.move(100, 200);
point.x;
// => 100
point.y;
// => 200
Definisci una classe ProgramWindow con i seguenti campi:
screenSize: contiene un valore fisso di tipo Size con width 800 e height 600size : contiene un valore di tipo Size, il valore iniziale è quello predefinito dell'istanza di Size
position : contiene un valore di tipo Position, il valore iniziale è quello predefinito dell'istanza di Position
Quando la finestra viene aperta (creata), all'inizio ha sempre la dimensione e la posizione predefinite.
const programWindow = new ProgramWindow();
programWindow.screenSize.width;
// => 800
// Similar for the other fields.
Nota a margine: il nome ProgramWindow è usato al posto di Window per distinguere la classe dalla classe Window integrata che esiste negli ambienti browser.
La classe ProgramWindow dovrebbe includere un metodo resize.
Dovrebbe accettare come input un parametro di tipo Size e tentare di ridimensionare la finestra alla dimensione specificata.
Tuttavia, la nuova dimensione non può superare certi limiti.
x = 400, y = 300 e viene richiesto un ridimensionamento a height = 400, width = 300, la finestra verrà ridimensionata a height = 300, width = 300, perché lo schermo non è abbastanza grande nella direzione y da soddisfare completamente la richiesta.const programWindow = new ProgramWindow();
const newSize = new Size(600, 400);
programWindow.resize(newSize);
programWindow.size.width;
// => 600
programWindow.size.height;
// => 400
Oltre alla funzionalità di ridimensionamento, la classe ProgramWindow dovrebbe includere anche un metodo move.
Dovrebbe accettare come input un parametro di tipo Position.
Il metodo move è simile a resize, ma questo metodo regola la posizione della finestra al valore richiesto, non la dimensione.
Come per resize, la nuova posizione non può superare certi limiti.
x che per y.x = 250, y = 100 e viene richiesto uno spostamento a x = 600, y = 200, la finestra verrà spostata a x = 550, y = 200, perché lo schermo non è abbastanza grande nella direzione x da soddisfare completamente la richiesta.const programWindow = new ProgramWindow();
const newPosition = new Position(50, 100);
programWindow.move(newPosition);
programWindow.position.x;
// => 50
programWindow.position.y;
// => 100
Implementa una funzione changeWindow che accetta come input un'istanza di ProgramWindow e modifica la finestra portandola alla dimensione e alla posizione specificate.
La funzione dovrebbe restituire l'istanza di ProgramWindow che è stata passata, dopo che le modifiche sono state applicate.
La finestra dovrebbe avere una larghezza di 400, un'altezza di 300 ed essere posizionata a x = 100, y = 150.
const programWindow = new ProgramWindow();
changeWindow(programWindow);
programWindow.size.width;
// => 400
// Similar for the other fields.
Iscriviti a Exercism per imparare e padroneggiare JavaScript con 37 concetti159 esercizi e il mentoring di persone reali, tutto gratis.