Programmazione ad oggetti con Function – Prototype

In JavaScript qualunque oggetto ha una struttura che lo definisce con proprietà e metodi nativi che non possiamo ne sovrascrivere ne modificare. Sebbene i valori nativi siano disponibili in sola lettura, possiamo aggiungerne di nuovi e manipolarli a piacimento.

Vediamo ora come applicare questo alla programmazione ad oggetti e scoprire quali vantaggi può offrirci.

Definiamo una semplice classe Person che costruiremo man mano ampliandone il suo prototype.

function Person () {};

Person.prototype // {constructor: function(){...}, ...}

Il suo prototype nativo conterrà solo il metodo constructor che viene invocato quando generiamo una nuova istanza e va ad eseguire il codice della classe stessa, e altri metodi ereditati dalla super classe Object.

Aggiungiamo due nuovi metodi per scrivere e leggere il nome della persona

Person.prototype.setName = function (value) { this.name = value };
Person.prototype.getName = function () { this.name = value };

var p = new Person();
p.setName("John");
p.getName(); // "John"

I metodi appena definiti sono diventati nativi per la funzione Person e quindi disponibili alle sue istanze e a ciascuna di queste andranno a rispettivamente a creare e leggere la proprietà name.

Possiamo migliorare l’efficienza di questi due metodi fondendoli in uno solo.

Person.prototype.name = function (value) {
  if (value) {
    this._name = value
  } else {
    return this._name
  }
}

In questa maniera ora avremo un solo metodo name al quale se passiamo un parametro setterà la proprietà altrimenti la leggerà.

var p = new Person();
p.name("John");
p.name(); // "John"

Importante. Abbiamo dovuto però rinominare this.name in this._name per evitare di fare confusione tra metodo e proprietà, altrimenti, una volta invocato il metodo name con un parametro in input andiamo a ridefinire cosa sia name stesso, trasformandolo in un valore e quindi non potendolo più usare successivamente come funzione, ottenendo in console un errore come il seguente.

var p = new Person();
p.name("John");
p.name(); // "Uncaught TypeError: p.name is not a function(…)"
p.name; // "John"
p.name("Smith"); // "Uncaught TypeError: p.name is not a function(…)"
p.name = "Smith";
p.name; // "Smith"

I vantaggi di aggiungere metodi al prototype di una function riguardano soprattutto l’aspetto computazione e occupazione di risorse di memoria. Infatti questi metodi, visti allo stesso livello di quelli nativi, vengono allocati una sola volta e associati ad ogni istanza successiva pur mantenendo il contesto indipendente l’una dall’altra.

In altre parole non viene allocato uno spazio di memoria per ogni metodo name di ogni eventuale var p1, p2, …, ma ne viene allocato uno solo al momento della definizione di Person e poi ereditato dalle istanze e su grandi numeri sia di metodi che di istanze è sicuramente da tenere a conto per le performance.

D’altro canto però per scambiare informazioni tra due metodi diversi o uno con condizioni all’interno come l’esempio precedente di name, dobbiamo per forza definire delle variabili di classe con il rischio innanzitutto di name overriding e in secondo luogo di rendere visibili delle proprietà che preferiremmo rimanessero nascoste all’utente.

Nell’esempio precedente infatti abbiamo voluto mettere a disposizione dell’utente un semplice metodo per manipolare la proprietà name ma per fare ciò abbiamo dovuto creare una proprietà _name di supporto per salvare e leggere il valore, rendendo a questo punto superfluo il metodo stesso.

Per situazioni del genere sicuramente una soluzione migliore potrebbe essere quella di definire all’interno della funzione Person stessa metodi e variabili di supporto nascoste, a patto di accettare un uso maggiore di memoria.

function Person () {
  var name;
  this.name = function (value) {
    if (value) name = value;
    else return name;
  }
}

var p = new Person();
p.name("John");
p.name(); // "John"
p.name; // function ...