Programmazione ad oggetti con Function – Ereditarietà

La programmazione ad oggetti ci offre la possibilità di definire un set di classi “base” da estendere di volta in volta per creare oggetti più complessi senza dover riscrivere ogni volta l’inizializzazione di proprietà condivise.

Riprendiamo le classi di esempio definite nell’articolo Programmazione ad oggetti con Function – Introduzione.

function Employee (params) {
  var params = params || {};
  this.id = params.id;
  this.name = params.name;
  this.company = params.company;
  this.role = params.role;
  this.manager = params.manager;
}
function Manager (params) {
  var params = params || {};
  this.id = params.id;
  this.name = params.name;
  this.company = params.company;
  this.employees = params.employees;
  this.project = params.project;
}

Nelle due classe così definite ci sono delle inizializzazioni che abbiamo dovuto riscrivere ma avremmo potuto evitare. Per questo motivo creiamo ora la classe Person che poi estenderemo all’interno di Employee e Manager.

function Person (params) {
  var params = params || {};
  this.id = params.id;
  this.name = params.name;
  this.company = params.company;
}

Per estendere questa classe per esempio dentro Employee potremo istanziare una variabile Person al suo interno, aggiungerci altre proprietà e metodi, infine ritornare l’istanza così estesa.

function Employee (params) {
  var params = params || {};
  var person = new Person(params);
  person.role = params.role;
  person.manager = params.manager;
  return person;
}

In questa maniera però pur ottenendo all’atto pratico ciò che vogliamo, cioè una istanza completa con le proprietà definite in Person e Employee, non stiamo definendo correttamente un’istanza di Employee.

var e = new Employee();
e instanceof Employee // false
e instanceof Person // true

Definire correttamente una variabile d’istanza può non sembrare importante perchè tutto sommato la variabile e comunque ha tutto quello di cui si ha bisogno, ma diventa cruciale se volessimo creare ad esempio una funzione che a seconda del tipo dell’istanza passata in input esegue va ad eseguire operazioni differenti.

function classFactory (input) {
  if (input instanceof Person) {
    ...
  } else if (input instanceof Employee) {
    ...
  }
  ...
}

Per risolvere questo problema, la classe nativa Function di JavaScript ci offre due metodi: callapply. Entrambi servono allo scopo di applicare al contesto specificato i valori passati in input senza cambiarne la natura, ma mentre il primo accetta singoli parametri, il secondo si aspetta di leggerli da un array.

function Employee (params) {
  var params = params || {};
  Person.call(this, params); // Person.apply(this, [params])
  this.role = params.role;
  this.manager = params.manager;
}

Come possiamo notare non abbiamo più bisogno di definire una variabile interna di appoggio da ritornare come abbiamo fatto prima, in quanto la funzione call (o apply) si occuperà prendere i valori contenuti dentro params, applicarli alla classe Person e mantenere this legata al contesto dell’istanza di Employee.

var e = new Employee();
e instanceof Employee // true
e instanceof Person // false

Il processo di estensione di oggetti può essere concatenato liberamente: è possible estendere classi che a loro volta estendono altre classi. Ad esempio se ridefinissimo la classe Manager con le stesse proprietà di Employee e in più quelle di project e employees, allora avremo questa situazione

function Manager (params) {
  ...
  Employee.call(this, params);
  ...
}
function Employee (params) {
  ...
  Person.call(this, params);
  ...
}
function Person (params) {
  ...
}

Inoltre possiamo estendere più classi contemporaneamente all’interno di una sola.

Supponiamo di avere a disposizione una classe per gestire le credenziali di un utente.

function UserCredential (params) {
  var params = params || {};
  this.user = params.user;
  this.password = this.password;
  this.resetUser = function () {...};
  this.resetPassword = function () {...};
}

Potremmo aggiornare la nostra classe Employee in questo modo

function Employee (params) {
  ...
  Person.call(this, params);
  UserCredential(this, params);
  ...
}

Da tenere bene a mente che se le classi che andiamo ad estendere offrono delle stesse proprietà allora l’ultima in ordine di linea di codice va a sovrascrivere le precedenti. Nel nostro caso UserCredential potrebbe sovrascrivere eventuali proprietà di Person.


Estendere una classe non significa che abbiamo solo la possibilità di sfruttarne variabili e metodi, ma si ha la possibilità anche di sovrascriverne il comportamento a piacimento. Possiamo infatti vedere la classe estesa come un insieme di valori di default, in modo che non sia sempre necessario passare tutte le informazioni per valorizzare le proprietà di una classe.

Ad esempio la classe Employee potrebbe sovrascrivere l’inizializzazione della proprietà id rispetto a Person

function Employee (params) {
  var params = params || {};
  Person.call(this, params); // Person.apply(this, [params]);
  this.id = Math.random();
  this.role = params.role;
  this.manager = params.manager;
}

In questo modo i nostri dipendenti avranno sempre un id valorizzato random pur passandone il valore.

var e1 = new Employee();
var e2 = new Employee({id: 0});

e1.id // Math.random()
e2.id // Math.random();