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.

// Initializing a constructor function
function Hero(name, level) {
    this.name = name;
    this.level = level;
}
constructor.js

Quando lo traduciamo nella sintassi della classe, mostrata di seguito, vediamo che è strutturato in modo molto simile.

// Initializing a class definition
class Hero {
    constructor(name, level) {
        this.name = name;
        this.level = level;
    }
}
class.js

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().

function Hero(name, level) {
    this.name = name;
    this.level = level;
}

// Adding a method to the constructor
Hero.prototype.greet = function() {
    return `${this.name} says hello.`;
}
constructor.js

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.

class Hero {
    constructor(name, level) {
        this.name = name;
        this.level = level;
    }

    // Adding a method to the constructor
    greet() {
        return `${this.name} says hello.`;
    }
}
class.js

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.

// Creating a new constructor from the parent
function Mage(name, level, spell) {
    // Chain constructor with call
    Hero.call(this, name, level);

    this.spell = spell;
}
constructor.js

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

// Creating a new class from the parent
class Mage extends Hero {
    constructor(name, level, spell) {
        // Chain constructor with super
        super(name, level);

        // Add a new property
        this.spell = spell;
    }
}
class.js

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.

function Hero(name, level) {
    this.name = name;
    this.level = level;
}

// Adding a method to the constructor
Hero.prototype.greet = function() {
    return `${this.name} says hello.`;
}

// Creating a new constructor from the parent
function Mage(name, level, spell) {
    // Chain constructor with call
    Hero.call(this, name, level);

    this.spell = spell;
}
constructor.js
// Initializing a class
class Hero {
    constructor(name, level) {
        this.name = name;
        this.level = level;
    }

    // Adding a method to the constructor
    greet() {
        return `${this.name} says hello.`;
    }
}

// Creating a new class from the parent
class Mage extends Hero {
    constructor(name, level, spell) {
        // Chain constructor with super
        super(name, level);

        // Add a new property
        this.spell = spell;
    }
}
class.js

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.