back to top

Javascript OOP: prototipi e classi

Il motore del linguaggio di programmazione Javascript è unico nel suo genere: è orientato agli oggetti, ma non nella classica modalità comune a tutti i linguaggi orientati agli oggetti, più "famosi", come ad esempio Java o C++.

Possiamo invece definire più accuratamente il motore di Javascript come "orientato ai prototipi" (o prototype-oriented / protortype-based), dato che ogni oggetto eredita funzionalità da un ulteriore oggetto definito appunto "prototipo".

Javascript: un linguaggio prototype-based

Prima di vedere nella pratica come è possibile utilizzare i prototipi per produrre gerarchie di oggetti all’interno di Javascript, è opportuna una panoramica teorica sugli stessi prototipi. Questo ci consente di utilizzare correttamente le nuove e moderne sintassi dedicate alle classi senza incorrere in grossolani errori.

In Javascript, ogniqualvolta viene creata una nuova funzione, il motore aggiungere automaticamente una proprietà "prototype" alla funzione stessa, che è un oggetto, e che indentifica di fatto il nostro prototipo. Di default, questo oggetto possiede una proprietà "constructor" che si riferisce/punta a sua volta alla funzione, ed una proprietà in formato oggetto denominata "__proto__", che invece punta alla proprietà "prototype" della funzione "constructor" appena analizzata. In questo modo, ogniqualvolta viene prodotta una nuova istanza utilizzando la funzione costruttore, la proprietà "__proto__" viene copiata nell’istanza insieme a tutte le altre proprietà e metodi. Possiamo verificare quanto appena detto tramite un semplice snippet di codice. Ad esempio, immaginiamo di creare una classe chiamata "Person" che definisce il costruttore per creare persone differenti sottoforma di oggetti istanza. Grazie al modello prototype-based di Javascript, agiremo nel seguente modo:

function Person(first, last, age, eyecolor) {
  this.firstName = first;
  this.lastName = last;
  this.age = age;
  this.eyeColor = eyecolor;
  this.getName = function() {
  return this.firstName + " " + this.lastName;
  }
}

var riccardo = new Person("Riccardo", "Degni", 32, "hazel");

Se effettuiamo il debug della variabile oggetto "riccardo", che è un’istanza della "classe" Person, creata tramite funzione costruttore, otterremo quanto segue:

Person {firstName: "Riccardo", lastName: "Degni", age: 32, eyeColor: "hazel", getName: }
age: 32
eyeColor: "hazel"
firstName: "Riccardo"
getName:  ()
lastName: "Degni"
__proto__:
constructor:  Person(first, last, age, eyecolor)
__proto__: Object

Questo oggetto prototype può ora essere utilizzato per aggiungere un qualsisi numero di proprietà e metodi alla funzione costruttore, che saranno immediatamente disponbili e condivisi da tutte le istanze della classe. Ad esempio, per aggiungere un metodo denominato "setFirstName" che permette di impostare dinamicamente la proprietà "firstName" delle singole istanze della classe person, lo aggiungeremo come metodo dell’oggetto prototype della classe Person, nella seguente modalità:

Person.prototype.setFirstName = function(name) {
  this.firstName = name;
};

A questo punto possiamo utilizzarlo sulle singole istanze:

riccardo.setFirstName("ricky");
riccardo.getName(); // stampa "ricky"

Questa straordinaria funzionalità ci permette, oltra a creare gerarchie di oggetti estensibili, anche di alterare gli oggetti di base di Javascript, perchè il modello orientato ai prototipi è applicato ed applicabile ovunque: Data eredita da Data.prototype, Array eredita da Array.prototype, Person eredita da Person.prototype, e cosi via. Ad esempio, ammettiamo di volere concretizzare la possibilità di utilizzare un metodo su tutti gli array che restituisca la somma di tutti i singoli valori in esso contenuti. Invece di usare funzioni singole, o peggio di ripetere manualmente l’operazione, possiamo estendere l’oggetto prototype del costruttore Array con il nostro metodo "sum", procedendo come segue:

Array.prototype.sum = function () {
  var total = 0;
  for (var i = 0; i < this.length; i++) {
    total += this[i];
  }
  return total;
};

Ora, dato che questo metodo viene ereditato, possiamo utilizzarlo con tutte le istanze del costruttore Array (ovvero con tutti gli array):

var myarr = [1, 2, 3];
console.log( myarr.sum() ); // 6

Avvalendosi di questa modalità sono stati creati tantissimi framework Javascript, ma occorre comunque sempre fare attenzione quando si alterano gli oggetti nativi, per non sovrascrivere o modificare funzionalità che potrebbero produrre risultati inaspettati.

Le classi: una sintassi per creare prototipi

Forti della comprensione relativa al modello orientato ai prototipi, possiamo ora studiare le feature offerte dalle classi in tutta sicurezza. Questo nuovo concetto ha indotto in fallo moltissimi sviluppatori che hanno erroneamente pensato ad un cambiamento nel motore del linguaggio di scripting Web più utilizzato al mondo.

Il concetto di "classe" è stato introdotto in Javascript con la specifica ECMAScript 2015, ma non definisce uno stravolgimento nelle meccaniche interne del linguaggio, bensì una nuova e più moderna sintassi con cui è possibile definire le medesime funzionalità analizzate in precedenza. Il modello basato sull’eredità dei prototipi rimane assolutamente inalterato ed invariato, ma abbiamo ora disponbile una modalità alternativa per scrivere il codice.

Vediamo quindi come possiamo definire la classe "Person" dichiarata ed utilizzata in precedenza, attraverso la nuova sintassi offerta da ECMAScript2015:

class Person {
  constructor(first, last, age, eyecolor) {
    this.firstName = first;
    this.lastName = last;
    this.age = age;
    this.eyeColor = eyecolor;
  }
  getName() {
    return this.firstName + " " + this.lastName;
  }
}

Come possiamo vedere, abbiamo ora a disposizione la keyword "class" che ci permette di produrre variabili che si riferiscono/puntano alla funzione costruttore definita all’interno della classe. La classe può contenere metodi e proprietà che andranno ad estendere l’oggetto prototype di Person, nella stessa modalità analizzata in precedenza. Possiamo dunque creare un’istanza ed utilizzare tutti i suoi metodi:

var riccardo = new Person("Riccardo", "Degni", 32, "hazel");
console.log(riccardo.getName());

Ci sono tuttavia delle differenze sintattiche (e funzionali) che vengono introdotte con questa nuova sintassi. Vediamole. La prima e più evidente differenza riguarda la modalità di definizione dei metodi, che non richiedono la keyword "function" e non sono separati da una virgola ",". Questo accade perchè la sintassi delle classi è differente da quella degli oggetti letterali, e i due concetti non identificano la medesima cosa. La seconda novità riguarda la definizione del metodo costruttore attraverso la keyword "constructor" che è ora esplicito nella costruzione della classe.

Passiamo alle differenze funzionali.

1) I metodi delle classi definite in questa modalità non sono enumerabili. In Javascript, ogni proprietà di un oggetto possiede infatti un flag "enumerable" indicante la disponbilità della proprietà ad essere utilizzata nei contesti di svariate operazioni, come ad esempio l’iterazione attraverso costrutto "for…in". Una classe imposta questo flag al valore booleano "false" per tutti i metodi definiti nel suo protoype.

2) Esiste la possiiblità di non dichiarare esplicitamente un costruttore, all’interno della classe. In questo caso viene aggiunto dal motore Javascript un costruttore di default vuoto constructor() {}, in una modalità simile al linguaggio Java.

3) Il codice all’interno di una classe è sempre eseguito in "strict mode". Questa caratteristica consente di produrre codice maggiormente performante e privo di imprecisioni, che verranno evetualmente mostrate come errori, al posto di passare inosservate come nel contesto esterno alla modalità restrittiva. Alcune keywords sono inoltre riservate, con uno sguardo alle future versioni delle specifiche ECMA.

4) Le dichiarazioni delle classi non sono sottoposte ad "hoisting". Con il termine hoisting in Javascript si intende quel comportamento del motore in cui tutte le dichiarazioni vengono automaticamente "spinte" all’inizio dello scoper corrente, che consente quindi di utilizzare una variabile o una funzione prima della sua dichiarazione.Il seguente codice produce infatti un errore di tipo "ReferenceError":

const riccardo = new Person(); // ReferenceError

class Person {}

5) Le classi non consentono i normali assegnamenti di valori a proprietà o metodi nella stessa modalità in cui agiscono le funzioni costruttori o gli oggetti letterali. E’ possibile invece utilizzare i cosiddetti "getters" e "setters" per impostare e recuperare valori, al posto del classico assegnamento "proprietà: valore".

Le funzionalità delle classi

Le nuove specifiche hanno portato svariate novità molto utili ed interessanti, che si possono utilizzare lavorando con le classi, e che si avvicinano molto al modello object-oriented di linguaggi Java-like. Tra queste, troviamo:

  • i getters ed i setters
  • i metodi statici
  • i membri privati
  • l’ereditarietà tramite la keywords "extends"
  • la keyword "super"

Getters e setters

Questi metodi particolari permettono di impostare delle funzioni che vengono eseguite rispettivamente quando si accede al valore di una proprietà (getter) e quando si imposta il valore di una proprietà (setter). In questo modo possiamo costruire elaborazioni più complesse mentre recuperiamo o impostiamo il valore di una proprietà di una classe. Vediamo un semplice esempio:

class Person {
  constructor(first, last, age, eyecolor) {
    this.firstName = first;
    this.lastName = last;
    this.age = age;
    this.eyeColor = eyecolor;
  }

  getName() {
    return this.firstName + " " + this.lastName;
  }

  get firstName() {
    return this.getName();
  }

  set firstName(value) {
    if (value.length < 4) {
      alert("nome troppo corto.");
      return;
    }
    this._firstName = value;
  }
}

var riccardo = new Person("Riccardo", "Degni", 32, "hazel");

// getName
console.log(riccardo.firstName);

riccardo.firstName = 'ric'; // alert

In questo caso abbiamo impostato per la proprietà "firstName" sia un getter, sia un setter. Quando accediamo alla suddetta proprietà, verrà richiamato il getter che a sua volta andrà a richiamare il metodo pubblico "getName", che restituirà il nome completo della persona istanza. Contrariamente, quando andremo ad impostare la proprietà "firstName", verrà richiamato il setter, che controllerà la lunghezza del parametro utilizzato come valore, e se questa sarà maggiore di 4 caratteri, imposterà la suddetta proprietà. Come possiamo intuire, sia i getter che i setter vanno a legare dei metodi particolari ad una proprietà, in fase di accesso ed in fase di scrittura. Ovviamente è possibile creare un’infinità di metodi getter e setter, anche aventi nomi differenti da quelli delle proprietà, per svolgere le più disparate operazioni, come la modifica di un array interno, il log di messaggi o la produzione di calcoli matematici.

I metodi statici

I metodi statici, come nel linguaggio di programmazione Java, sono metodi che appartengono alla classe, piuttosto che alle singole istanze, e quindi possono essere utilizzati solo attraverso il riferimento della prima. Di solito vengono utilizzati per produrre utility necessarie ad operazioni condivise da tutte le istanze. Ad esempio:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static distance(a, b) {
    const dx = a.x - b.x;
    const dy = a.y - b.y;
    return Math.hypot(dx, dy);
  }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);

console.log(Point.distance(p1, p2));

La classe "Point" definisce il metodo statico "distance", che andrà a calcolare la distanza tra due oggetti di classe Point. La cosa interessante sta nel fatto che andremo ad utilizzare la classe per accedere al suddetto metodo, tramitePoint.distance e non le singole istanze (p1.distance). All’interno di un metodo statico, il valore di "this" è "undefined".

I membri privati

Con l’avvento delle classi, possiamo ora definire sia membri pubblici, sia membri privati. Nelle classi che abbiamo visto in precedenza, abbiamo settato le proprietà delle istanze direttamente all’interno del costruttore, senza un’esplicita dichiarazione pubblica. Possiamo invece dichiarare questi campi direttamente all’interno della classe, per rendere il codice maggiormente documentato, ed identificare la presenza dei campi. A questi può essere associato opzionalmente un valore di default. Ad esempio:

class Person {
  firstName = 0;
  sex = "m";
  constructor(firstName, sex) {    
    this.firstName = firstName;
    this.sex = sex;
  }
}

Contrariamente ai membri pubblici, i membri privati non sono accessibili dall’esterno della classe, ovvero sono utilizzabili solo all’interno del corpo della classe stessa. Per dichiarare un membro privato, che deve essere espresso esplicitamente con la notazione vista poc’anzi, basta aggiungere il simbolo del cancelletto ("#") prima del nome della proprietà, come ad esempio:

class Person {
  #firstName = 0;
  #sex = "m";
  constructor(firstName, sex) {    
    this.#firstName = firstName;
    this.#sex = sex;
  }
}

A differenza delle proprietà pubbliche, se tentiamo di accedere ad un membro privato al di fuori della classe di appartenenza, verrà generato un errore. Nota: i membri privati non possono essere dichiarati successivamente alla creazione della classe.

L’ereditarietà tramite la keywords "extends"

Una delle caratteristiche più importanti e funzionali della Object-Oriented Programming è senza dubbio l’ereditarietà. Con questo termine si intende la possibilità di estendere una classe parente attraverso una classe "figlia", che eredita tutti i membri pubblici della precedente struttura. In questo modo possiamo sia evitare dichiarazioni ridondanti, andando a produrre codice estremamente modulare, sia ridefinire funzionalità ad hoc nel caso in cui le classi figlie necessitino di funzionalità aggiuntive. Per estendere una classe, occorre utilizzare la keyword "extends", nella seguente modalità:

class Male extends Person {
  constructor(first, last, age, eyecolor) {
    super(first, last, age, eyecolor);
    this.sex = "M";
  }
}

var riccardo = new Male("Riccardo", "Degni", 32, "hazel");
   
console.log(riccardo.sex);
console.log(riccardo.name);

In questo caso la variabile "riccardo" non è più un’istanza della super-classe Person, ma piuttosto lo è della sotto-classe "Male" (maschio) che eredita le funzionalità dalla struttura parente, come ad esempio il getter "name", e ne ridefinisce di nuove, come ad esempio la proprietà pubblica "sex". Grazie all’ereditarietà possiamo creare gerarchie di classi anche molto complesse, andando a produrre codice molto simile a quello proveniente da linguaggi full object-oriented, ma sempre utilizzando il motore basato sui prototipi.

La keyword "super"

Una volta che abbiamo costruito una gerarchia di classi, è possibile che una sotto-classe (o classe figlia) vada a ridefinire una proprietà o un metodo implementando nuove funzionalità. Questo dovrebbe significare che la proprietà o il metodo appartenente alla super-classe (o classe parente) non è più accessibile nella sotto-classe. Sbagliato! Tramite la keyword "super" possiamo ad esempio richiamare i metodi della super-classe, anche se li abbiamo ridefiniti nelle nuove classi. Ad esempio:

class Male extends Person {
  constructor(first, last, age, eyecolor) {
    super(first, last, age, eyecolor);
    this.sex = "M";
  }

  getName() {
    let n = super.getName();
    return "il nome di questa istanza è: " + n;
  }

}

var riccardo = new Male("Riccardo", "Degni", 32, "hazel");
   
console.log(riccardo.getName());

Nella classe "Male" siamo andati a ridefinire il metodo denominato "getName". Tuttavia non volevamo eliminare la precedente funzionalità, che restituiva una stringa contenente nome e cognome relativi all’istanza: per questo motivo l’abbiamo richiamata attraverso la keyword "super" e abbiamo successivamente restituito una stringa ancora più formattata, che ha unito la capacità del super-metodo a quella del sotto-metodo.

Le classi come espressioni

Infine, diamo uno sguardo alla sintassi alternativa con cui è possibile dichiarare le classi in Javascript. Proprio come nel caso delle funzioni infatti, anche le classi possono essere dichiarate tramite espressione. Questo significa che anche le dichiarazioni delle classi possono essere dinamiche: posizionate in espressioni, restituite, passate come parametro, e cosi via. Questo comportamente è del tutto normale, se ricordiamo che le classi sono semplicemente una "forma speciale" con cui si utilizza una funzione-prototype.

Ad esempio, possiamo dichiarare una classe nella seguente modalità:

let User = class {
  sayHello() {
    alert("Hello");
  }
};

let user1 = new User();
user1.sayHello();

Alla variabile "User" viene assegnata una classe, e quindi possiamo utilizzarla normalmente come costruttore per le singole istanze. Infine, la variabile "user1", essendo un’istanza di "User", può utilizzare i suoi e metodi e le sue proprietà, come ad esempio il metodo "sayHello".

Conclusioni

La possibilità di scrivere codice Javascript mediante classi ha aperto un nuovo modo di sviluppare lato client, un modo sempre più simile a quello dei linguaggi interamente Object-Oriented come Java. Questa funzionalità non deve, tuttavia, trarre lo sviluppatore in errore, perchè il modello basato sui prototipi resta il vero attore che agisce dietro le quinte, potenziato ed esteso, ma sempre fondamentale e centrale. E’ opportuno comprendere a fondo come Javascript lavora con i prototipi, prima di cimentarsi nel codice a classi. Inoltre, dato che le classi sono di fatto una specifica "nuova", i vecchi browser non possiedono la capacità di elaborarle correttamente, e per questo motivo vanno considerate ancora funzionalità "moderne", da utilizzare con le versioni più recenti dei maggiori browser.

Tuttavia le classi sono un potentissimo strumento del futuro, ed è fondamentale per lo sviluppatore Javascript/front-end moderno saperle utilizzare nella modalità corretta per produrre applicazioni di nuova generazione sempre più performanti.

Pubblicitร 

Leggi anche...

Infinite scroll, come programmarlo su AMP e su Web con Javascript

L'infinite scroll è una tecnica di design e navigazione...

Codice Fiscale: 5 javascript per la verifica e il calcolo

Il codice fiscale รจ un identificativo tributario univoco che...

Math.ceil() – Arrotondare per eccesso con Javascript

Il metodo ceil() dell'oggetto Math di Javascript è utilizzato...

Minificare Javascript: librerie e strumenti online per comprimere il sorgente JS

La minificazione è un processo abbastanza diffuso nell'implementazione delle...

Javascript: svuotare un campo input o una textarea con un click

Quando si fornisce agli utenti un modulo per l'inserimento...

6 video player HTML5 per il tuo sito web

Con il rilascio delle specifiche definitive per HTML5 molte...
Pubblicitร