Introduzione
JavaScript è un linguaggio prototype-based e ogni oggetto in JavaScript ha una proprietà interna nascosta chiamata [[Prototype]]
che può essere utilizzata per estendere proprietà e metodi degli oggetti.
La specifica linguistica ECMAScript 2015, spesso definita come ES6, ha introdotto le classi nel linguaggio JavaScript. Le classi in JavaScript in realtà non offrono funzionalità aggiuntive e sono spesso descritte come "syntactical sugar" rispetto ai prototypes e all'eredità in quanto offrono una sintassi più pulita ed elegante. Poiché altri linguaggi di programmazione utilizzano le classi, la sintassi della classe in JavaScript rende più semplice per gli sviluppatori spostarsi tra i vari linguaggi.
Le classi sono funzioni
Una classe JavaScript è un tipo di funzione. Le classi sono dichiarate con la parola chiave class
. Useremo la sintassi dell'espressione della funzione per inizializzare una sintassi dell'espressione della funzione e della classe per inizializzare una classe.
// Initializing a function with a function expression
const x = function() {}
// Initializing a class with a class expression
const y = class {}
Possiamo accedere a [[Prototype]]
di un oggetto utilizzando il metodo Object.getPrototypeOf()
. Usiamolo per testare la funzione vuota che abbiamo.
Object.getPrototypeOf(x);
ƒ () { [native code] }
Possiamo anche usare quel metodo sulla classe che abbiamo appena creato.
Object.getPrototypeOf(y);
ƒ () { [native code] }
Il codice dichiarato con function
e class
restituiscono entrambi una funzione [[Prototype]]
. Con i prototypes, qualsiasi funzione può diventare un'istanza del costruttore utilizzando la parola chiave new
.
const x = function() {}
// Initialize a constructor from a function
const constructorFromFunction = new x();
console.log(constructorFromFunction);
x {}
constructor: ƒ ()
Questo vale anche per le classi.
const y = class {}
// Initialize a constructor from a class
const constructorFromClass = new y();
console.log(constructorFromClass);
y {}
constructor: class
Questi esempi di costruttori di prototypes sono altrimenti vuoti, ma possiamo vedere la sintassi, entrambi i metodi ottengono lo stesso risultato finale.
Definire una classe
Una funzione constructor viene inizializzata con un numero di parametri, che verrebbero assegnati come proprietà di this
, facendo riferimento alla funzione stessa. La prima lettera dell'identificatore sarebbe in maiuscolo per convenzione.
Quando lo traduciamo nella sintassi della classe, mostrata di seguito, vediamo che è strutturato in modo molto simile.
Sappiamo che una funzione constructor è pensata per essere un object blueprint con la prima lettera dell'inizializzatore in maiuscolo (che è facoltativa). La parola chiave class
comunica in modo più diretto l'obiettivo della nostra funzione.
L'unica differenza nella sintassi dell'inizializzazione consiste nell'usare la parola chiave class
anziché function
nell'assegnare le proprietà all'interno di un metodo constructor()
.
Definire i metodi
La pratica comune con le funzioni constructors consiste nell'assegnare i metodi direttamente a prototype
anziché nell'inizializzazione, come si vede nel metodo seguente greet()
.
Con le classi questa sintassi è semplificata e il metodo può essere aggiunto direttamente alla classe. Utilizzando la scorciatoia di definizione del metodo introdotta in ES6, la definizione di un metodo è un processo ancora più conciso.
Diamo un'occhiata a queste proprietà e metodi in azione. Creeremo una nuova istanza di Hero
utilizzando la parola chiave new
e assegneremo alcuni valori.
const hero1 = new Hero('Varg', 1);
Se stampiamo ulteriori informazioni sul nostro nuovo oggetto con console.log(hero1)
, possiamo vedere maggiori dettagli su ciò che sta accadendo con l'inizializzazione della classe.
Hero {name: "Varg", level: 1}
__proto__:
▶ constructor: class Hero
▶ greet: ƒ greet()
Possiamo vedere nell'output che le funzioni constructor()
e greet()
sono state applicate a __proto__
, o [[Prototype]]
di hero1
, e non direttamente come metodo sull'oggetto hero1
. Mentre questo è chiaro nella creazione di funzioni constructors, non è ovvio durante la creazione di classi. Le classi consentono una sintassi più semplice e concisa, ma sacrificano un po' di chiarezza nel processo.
Estendere una classe
Una caratteristica vantaggiosa delle funzioni e delle classi del costruttore è che possono essere estese in nuovi schemi di oggetti basati sul padre (parent). Ciò impedisce la ripetizione del codice per oggetti simili ma che richiedono alcune funzionalità aggiuntive o più specifiche.
Nuove funzioni di constructor possono essere create dal parent usando il metodo call()
. Nell'esempio seguente, creeremo una classe di caratteri più specifica chiamata Mage
e assegneremo le proprietà di Hero
ad essa usando call()
, oltre ad aggiungere una proprietà aggiuntiva.
A questo punto, possiamo creare una nuova istanza di Mage
utilizzo delle stesse proprietà di Hero
e una nuova aggiunta.
const hero2 = new Mage('Lejon', 2, 'Magic Missile');
Avviando hero2
da console, possiamo vedere che abbiamo creato un nuovo Mage
basato sul costruttore.
Mage {name: "Lejon", level: 2, spell: "Magic Missile"}
__proto__:
▶ constructor: ƒ Mage(name, level, spell)
Con le classi ES6, la parola chiave super
viene utilizzata al posto di call
per accedere alle funzioni principali. Useremo extends
per fare riferimento alla classe genitore (parent).
Ora possiamo creare una nuova istanza Mage
nello stesso modo.
const hero2 = new Mage('Lejon', 2, 'Magic Missile');
Stamperemo hero2
sulla console e visualizzeremo l'output.
Mage {name: "Lejon", level: 2, spell: "Magic Missile"}
__proto__: Hero
▶ constructor: class Mage
L'output è quasi esattamente lo stesso, tranne che nel constructor della classe [[Prototype]]
è collegato al genitore, in questo caso Hero
.
Di seguito è riportato un confronto affiancato dell'intero processo di inizializzazione, aggiunta di metodi ed ereditarietà di una funzione di constructors e di una classe.
Sebbene la sintassi sia piuttosto diversa, il risultato sottostante è quasi lo stesso tra entrambi i metodi. Le classi ci danno un modo più conciso di creare object blueprints e le funzioni di constructors descrivono più accuratamente ciò che sta accadendo.
Conclusione
In questo tutorial, abbiamo appreso le somiglianze e le differenze tra le funzioni del costruttore JavaScript e le classi ES6. Sia le classi che i costruttori imitano un modello di ereditarietà orientato agli oggetti su JavaScript, che è un linguaggio di ereditarietà basato su prototype.
Comprendere prototypical inheritance è fondamentale per essere uno sviluppatore JavaScript efficace. Conoscere le classi è estremamente utile, poiché le librerie JavaScript popolari come React fanno un uso frequente della sintassi class
.