Tracce
/
JavaScript
JavaScript
/
Programma
/
Prototipi e classi
Pr

Prototipi e classi in JavaScript

32 esercizi

Informazioni su Prototipi e classi

JavaScript include le capacità 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 sono chiamate campi nel contesto della OOP, mentre le proprietà che contengono funzioni sono chiamate metodi.

JavaScript non aveva affatto le classi prima che venissero aggiunte alla specifica del linguaggio nel 2015, ma permetteva la programmazione orientata agli oggetti tramite l'ereditarietà basata sui prototipi. Ed 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, torneremo indietro al tempo in cui le classi non esistevano.

La sintassi dei prototipi

La funzione costruttrice

In JavaScript, il modello (la classe) è realizzato tramite una normale funzione. Quando una funzione deve essere usata come modello di questo tipo, viene chiamata funzione costruttrice e la convenzione è che il nome della funzione inizi con una lettera maiuscola. Le istanze (gli oggetti) vengono derivate 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 tra loro 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.

Dal 2015, si può accedere a [[prototype]] tramite Object.getPrototypeOf(). Prima, in molti ambienti era accessibile tramite la chiave __proto__.

Non confondere il prototipo di un oggetto ([[prototype]]) con la proprietà prototype della funzione costruttrice.

Note

Riassumendo:

  • I costruttori in JavaScript sono funzioni normali.
  • Costruire una nuova istanza crea un oggetto con una relazione verso il proprio costruttore, chiamata il suo prototipo.
  • Le funzioni sono oggetti (oggetti richiamabili) e quindi possono avere proprietà.
  • La proprietà prototype del costruttore (della funzione) diventerà il prototipo dell'istanza.

I campi delle istanze

Spesso vuoi che tutti gli oggetti derivati (le istanze) includano alcuni campi e che a questi vengano passati dei valori iniziali quando l'oggetto viene costruito. Ciò è possibile 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 questa 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 delle istanze

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 this è impostato sull'oggetto su cui il metodo viene 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

La catena dei prototipi

myCar nell'esempio precedente è un normale oggetto JavaScript, e se lo ispezionassimo (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 qui è la catena dei prototipi. Quando provi ad accedere a una qualsiasi proprietà (campo o metodo) di un oggetto, JavaScript controlla prima se la chiave in questione è presente direttamente nell'oggetto stesso. In caso contrario, continua a cercare la chiave nell'oggetto a cui punta la proprietà [[prototype]] dell'oggetto originale. Come detto 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 lì. La proprietà [[prototype]] di Car.prototype (myCar.[[prototype]].[[prototype]]) fa riferimento a Object.prototype (la proprietà prototype della funzione costruttrice Object). Contiene metodi generici disponibili per tutti gli oggetti JavaScript, ad esempio toString(). Il [[prototype]] di Object di solito è null, quindi la catena dei prototipi finisce lì. In conclusione, puoi chiamare myCar.toString() e quel metodo esisterà perché JavaScript lo cerca lungo tutta la catena dei prototipi. Puoi trovare un esempio dettagliato nell'articolo MDN «Inheritance and the prototype chain».

Caution

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.

Metodi dinamici (aggiungere metodi a tutte le istanze esistenti)

JavaScript ti permette di aggiungere metodi a tutte le istanze esistenti, anche dopo che sono state create.

Abbiamo visto che ogni istanza mantiene un riferimento alla proprietà prototype della funzione costruttrice. Questo significa che se aggiungi una voce a quell'oggetto prototype, la nuova voce (ad esempio un nuovo metodo) è subito disponibile per tutte le istanze create da quella funzione costruttrice.

function Car() {
  this.engineRunning = false;
}

const myCar = new Car();
// Calling myCar.startEngine() here would result in "TypeError:
// myCar.startEngine is not a function".

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

myCar.startEngine();
// This works, even though myCar was created before the method
// was added.

In teoria, i metodi dinamici possono persino estendere la funzionalità di oggetti integrati come Object o Array modificandone il prototipo. Questa tecnica è chiamata monkey patching. Poiché questa modifica influisce sull'intera applicazione, è meglio evitarla per prevenire effetti collaterali indesiderati. L'unico caso d'uso ragionevole è fornire un polyfill per un metodo mancante in ambienti più vecchi.

La sintassi delle 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, offre 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.

Dichiarazioni di classe

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 proprio 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

Come per le dichiarazioni di funzione e le espressioni di funzione, JavaScript supporta anche le espressioni di classe oltre alla dichiarazione di classe mostrata sopra.

Tieni presente che, dietro le quinte, JavaScript è ancora un linguaggio basato sui prototipi. Tutti i meccanismi che abbiamo visto nella sezione «La sintassi dei prototipi» qui sopra valgono ancora.

Campi privati, getter e setter

Per impostazione predefinita, in JavaScript tutti i campi delle istanze sono pubblici. È possibile accedervi e assegnarvi valori direttamente.

L'aggiunta di veri campi privati alla specifica del linguaggio è ancora in corso, vedi il documento della proposta per i dettagli.

Nel frattempo, puoi seguire la 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.

Ai campi privati a volte si accompagnano 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.

Campi e metodi di classe

Nella OOP, a volte vuoi fornire campi o metodi di utilità che non dipendono dalla specifica istanza. Sono invece definiti per la classe stessa. Questo si può ottenere con la parola chiave static.

class Car {
  static type = 'vehicle';

  static isType(targetType) {
    return targetType === 'vehicle';
  }
}

Car.type;
// => 'vehicle'

Car.isType('road sign');
// => false

L'ereditarietà basata sulle classi

Oltre al tipo di ereditarietà lungo la catena dei prototipi che abbiamo visto prima, in JavaScript puoi anche rappresentare l'ereditarietà tra classi. Questo è trattato nel concetto Ereditarietà.


  1. 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) ↩

Modifica tramite GitHub Il collegamento si apre in una nuova finestra o scheda

Impara Prototipi e classi