MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

Dettagli del modello a oggetti

Il contenuto di questo articolo è in discussione. Lascia un feedback e aiutaci a rendere migliore questa pagina: bug 1201380.

JavaScript è un linguaggio ad oggetti basato su prototipi, piuttosto che sulle classi. A causa di questa diversa base, può essere meno evidente come JavaScript permette di creare gerarchie di oggetti e di avere l'ereditarietà delle proprietà e dei loro valori. Questo articolo cerca di chiarire questo aspetto.

Questo capitolo presuppone una certa familiarità con JavaScript e con l'uso delle funzioni per la creazione di semplici oggetti.

Linguaggi class-based vs. linguaggi prototype-based

I linguaggi ad oggetti basati su classi, come Java e C++, si basano su due entità distinte: le classi (classes) e le istanze (instances).

  • Una classe definisce tutte le proprietà (considering methods and fields in Java, or members in C++, to be properties) che caratterizzano una determinata collezione di oggetti. Una classe è un'entità astratta, più che un membro del gruppo di oggetti che descrive. Per esempio, la classe Employee può rappresentare il gruppo di tutti i dipendenti.
  • Un'istanza, d'altra parte, è l'istanziazione di una classe; cioè uno dei suoi membri. Per esempio Victoria può essere un'istanza della classe Employee, che rappresenta un particolare individuo come un dipendente. Un'istanza ha esattamente le stesse proprietà della classe a cui appartiene (né più, né meno).

Un linguaggio basato su prototipi, come JavaScript, non fa questa distinzione: ha solo oggetti. Introduce la nozione di oggetto prototipo (prototypical object), un oggetto usato come modello da cui prendere le proprietà iniziali per un nuovo oggetto. Ogni oggetto può specificare le sue proprietà, anche quando viene creato o in fase di esecuzione.Inoltre, ogni oggetto può essere associato ad un altro oggetto come prototipo, consentendo al secondo oggetto di condividere le proprietà del primo.

Definizione di una classe

Nei linguaggi basati su classi, le classi vengono definite in class definition separate. In queste definizioni è possibile specificare metodi speciali, chiamari costruttori (constructors), per creare istanze della classe. Un costruttore può specificare i valori iniziali per le proprietà dell'istanza ed eseguire altre elaborazioni adatte al momento della creazione. Per creare le istanze di una classe si utilizza l'operatore new associato al metodo costruttore.

JavaScript segue un modello simile, ma non prevede la definizione della classe separata dal costruttore. Invece, per creare oggetti con un particolare set di proprietà e valori si definisce una funzione costruttore. Ogni funzione JavaScript può essere usata come costruttore. Per creare un nuovo oggetto si utilizza l'operatore new associato a una funzione costruttore.

Sottoclassi e ereditarietà

In un linguaggio basato su classi, si crea una gerarchia tra le classi attraverso le definizioni delle classi stesse. All'interno della definizione di una classe è possibile specificare che la nuova classe è una sottoclasse (subclass) di una classe esistente. La sottoclasse eredita tutte le proprietà della superclasse e può inoltre aggiungere nuove proprietà o modificare quelle ereditate. Per esempio, assumiamo che la classe Employee include solo le proprietà name e dept, e Manager è una sottoclasse di Employee che aggiunge la proprietà reports. In questo caso, un'istanza della classe Manager avrà tutte e tre le proprietà: name, dept, e reports.

JavaScript implementa l'ereditarietà permettendo di associare un oggetto prototipo ad ogni funzione costruttore. Quindi, è possibile ricreare esattamente l'esempio visto in precedenza, ma usando una terminologia leggermente diversa. Innanzitutto  si definisce la funzione costruttore Employee, specificando le proprietà name e dept. In seguito, si definisce la funzione costruttore Manager, chiamando il costruttore Employee e specificando la proprietà reports. Infine, si assegna a un nuovo oggetto derivato da Employee.prototype come il prototipo per la funzione costruttore Manager. Quando si crea un nuovo Manager, questo eredita le proprietà name e dept dall'oggetto Employee.

Aggiungere e rimuovere proprietà

Nei linguaggi basati su classi, una classe viene solitamente creata in fase di compilazione mentre le istanze possono essere create in fase di compilazione o in fase di esecuzione. Non è possibile cambiare il numero o il tipo di proprietà di una classe dopo che questa è stata definita. In JavaScript in fase di esecuzione si possono aggiungere o rimuovere proprietà a qualunque oggetto. Se si aggiunge una proprietà a un oggetto che è usato come prototipo per un gruppo di oggetti, anche gli oggetti del gruppo ereditano la nuova proprietà.

Riepilogo delle differenze

La tabella seguente fornisce un breve riepilogo di alcune di queste differenze. Il resto di questo capitolo descrive nel detteglio l'uso in JavaScript di costruttori e prototipi per creare una gerarchia di oggetti e lo confronta con la procedura che si userebbe in Java.

Confronto tra il sistema di oggetti basato su classi (Java) e il sistema di oggetti basato su prototipi (JavaScript)
Class-based (Java) Prototype-based (JavaScript)
Classi e istanze sono entità separate. Tutti gli oggetti possono ereditare da un altro oggetto.
Definire una classe con una definizione; istanziare una classe con un metodo costruttore Definire e creare una collezione di oggetti con una funzione costruttore.
Creare un singolo oggetto con l'operatore new. Uguale.
Costruire una gerarchia di oggetti usando la definizione di classe per definire le sottoclassi di classi esistenti. Costruire una gerarchia di oggetti assegnando un oggetto come prototipo associato a una funzione costruttore.
Ereditare le proprietà seguendo la catena delle classi. Ereditare le proprietà seguendo la catena dei prototipi.
La definizione di classe specifica tutte le proprietà di tutte le istanze della classe. Non è possibile aggiungere proprietà dinamicamente durante l'esecuzione. La funzione costruttore o il prototipo specificano un set iniziale di proprietà. È possibile aggiungere o rimuovere dinamicamente proprietà ai singoli oggetti o all'intero gruppo di oggetti.

L'esempio 'dipendente'

Il resto di questo capitolo usa la gerarchia dei dipendenti mostrata nella figura seguente.

Una semplice gerarchia con gli oggetti seguenti:

  • Employee ha le proprietà name (il cui valore di default è una stringa vuota) e dept (il cui valore di default è "general").
  • Manager è basato su Employee. Aggiunge la proprietà reports  (il cui valore di default è un array vuoto, destinato a contenere una serie di oggetti Employee).
  • WorkerBee è anch'esso basato su Employee. Aggiunge la proprietà projects (il cui valore di default è un array vuoto, destinato a contenere una serie di stringhe).
  • SalesPerson è basato su WorkerBee. Aggiunge la proprietà quota (il cui valore di default è 100). Sovrascrive inoltre la proprietà dept con il valore "sales", che indica che tutti i venditori si trovano nello stesso dipartimento.
  • Engineer è basato su WorkerBee. Aggiunge la proprietà machine (il cui valore di default è una stringa vuota) e sovrascrive la proprietà dept con il valore "engineering".

Creazione della gerarchia

Ci sono diversi modi per definire una funzione costruttore appropriata per implementare la gerarchia dei dipendenti. Il modo scelto per definirli dipende molto da cosa si vuole riuscire a fare nella propria applicazione.

Questa sezione mostra come usare definizioni molto semplici (e relativamente rigide) per dimostrare come far funzionare l'ereditarietà. In queste definizioni, quando si crea un oggetto non è possibile specificare il valore di nessuna proprietà. L'oggetto creato avrà semplicemente i valori di default, che potranno essere cambiati in un secondo momento.

In una applicazione reale, probabilmente si vorranno definire dei costruttori che permettono di impostare i valori delle proprietà mentre si crea un oggetto (per maggiori informazioni si veda la sezione Costruttori più flessibili). Per adesso, queste semplici definizioni dimostrano come le proprietà vengono ereditate.

Le seguenti definizioni Java e JavaScript di Employee sono simili. L'unica differenza è che in Java si ha la necessità di specificare il tipo di ogni proprietà (questo è dovuto al fatto che Java è un linguaggio fortemente tipizzato mentre JavaScript è un linguaggio a tipizzazione dinamica.

JavaScript

function Employee() {
  this.name = "";
  this.dept = "general";
}

Java

public class Employee {
   public String name = "";
   public String dept = "general";
}

Le definizioni di Manager e WorkerBee mostrano la differenza nel modo di specificare l'oggetto seguente che si trova più in alto nella catena di ereditarietà. In JavaScript si aggiunge un'istanza prototipo come valore della proprietà prototype della funzione costrutore. È possibile farlo in qualsiasi momento dopo aver definito il costruttore. In Java, si specifica la superclasse all'interno della definizione della classe. Non è possibile cambiare la superclasse all'esterno della definizione.

JavaScript

function Manager() {
  Employee.call(this);
  this.reports = [];
}
Manager.prototype = Object.create(Employee.prototype);

function WorkerBee() {
  Employee.call(this);
  this.projects = [];
}
WorkerBee.prototype = Object.create(Employee.prototype);

Java

public class Manager extends Employee {
   public Employee[] reports = new Employee[0];
}



public class WorkerBee extends Employee {
   public String[] projects = new String[0];
}


Le definizioni di Engineer e SalesPerson creano oggetti che discendono da  WorkerBee, e quindi da Employee. Un oggetto di questo tipo ha le proprietà di tutti gli oggetti che si trovano sopra di esso nella catena. Inoltre, queste definizioni sovrascrivono il valore ereditato delle proprietà  dept con nuovi valori specifici per questi oggetti.

JavaScript

function SalesPerson() {
   WorkerBee.call(this);
   this.dept = "sales";
   this.quota = 100;
}
SalesPerson.prototype = Object.create(WorkerBee.prototype);

function Engineer() {
   WorkerBee.call(this);
   this.dept = "engineering";
   this.machine = "";
}
Engineer.prototype = Object.create(WorkerBee.prototype);

Java

public class SalesPerson extends WorkerBee {
   public String dept = "sales";
   public double quota = 100.0;
}


public class Engineer extends WorkerBee {
   public String machine;
   public String dept = "engineering";
   public String machine = "";
}

Usando queste definizioni è possibile creare istanze di questi oggetti che ricevono i valori di default delle loro proprietà. La figura seguente illustra l'utilizzo di queste definizioni JavaScript per creare nuovi oggetti e mostra i valori delle diverse proprietà per questi nuovi oggetti.

Nota: Il termine istanza ha uno specifico significato tecnico nei linguaggi basati su classi. In questi linguaggi, un'istanza è una singola istanziazione di una classe ed è fondamentalmente differente dala classe. In JavaScript, "istanza" non ha questo significato tecnico perché JavaScript non ha questa differenza tra classi e istanze. Tuttavia, parlando di JavaScript, "istanza" può essere usato in modo informale per riferirsi a un oggetto creato usando una particolare funzione costruttore. Quindi, in questo esempio, è possibile dire che jane è un'istanza di Engineer. Allo stesso modo, sebbene i termini genitore  (parent), figlio (child), antenato (ancestor) e discendente (descendant) non hanno un significato conenzionale in JavaScript, possono essere usati in modo informale per riferirsi a oggetti che si trovano più o meno in alto nella catena dei prototipi.

Creazione di oggetti con definizioni semplici

Gerarchia degli oggetti

La gerarchia seguente è creata usando il codice sulla destra.

 

singoli oggetti

var jim = new Employee;
// jim.name is ''
// jim.dept is 'general'

var sally = new Manager;
// sally.name is ''
// sally.dept is 'general'
// sally.reports is []

var mark = new WorkerBee;
// mark.name is ''
// mark.dept is 'general'
// mark.projects is []

var fred = new SalesPerson;
// fred.name is ''
// fred.dept is 'sales'
// fred.projects is []
// fred.quota is 100

var jane = new Engineer;
// jane.name is ''
// jane.dept is 'engineering'
// jane.projects is []
// jane.machine is ''

Proprietà degli oggetti

Questa sezione spiega come gli oggetti ereditano le proprietà da altri oggetti presenti nella catena dei prototipi e cosa succede quando viene aggiunta una proprietà in fase di esecuzione.

Ereditare le proprietà

Supponiamo di creare un nuovo oggetto WorkerBee chiamato mark con l'istruzione seguente:

var mark = new WorkerBee;

Quando JavaScript vede l'operatore new, crea un nuovo oggeto generico e lo passa come valore della parola chiave this nella funzione costruttore WorkerBee. La funzione costruttore imposta esplicitamente il valore della proprietà projects e implicitamente il valore della proprietà interna __proto__ uguale al valore WorkerBee.prototype. (Il nome di questa proprietà ha due underscores iniziali e due finali). La proprietà __proto__ determina la catena di prototipi usata per restituire i valori delle proprietà. Una volta che queste proprietà sono impostate, JavaScript restituisce il nuovo oggetto e l'istruzione di assegnazione imposta la variabile mark per questo oggetto.

Questo processo non inserisce esplicitamente valori nell'oggetto mark (valori locali) per le proprietà che l'oggetto eredita dalla catena dei prototipi. Quando si richiede un valore di una proprietà, JavaScript prima controlla se il valore è presente nell'oggetto. Se c'è, viene restituito quel valore. Se il valore non è presente a livello locale, JavaScript controlla la catena dei prototipi (sfruttando la proprietà __proto__). Se un oggetto nella catena dei prototipi ha un valore per la proprietà, viene restituito. Se non viene trovata la proprietà, JavaScript risponde che l'oggetto non ha la proprietà cercata. In questo modo, l'oggetto mark ha le seguenti propietà con i rispettivi valori:

mark.name = "";
mark.dept = "general";
mark.projects = [];

L'oggetto mark eredita i valori per le proprietà name e dept dall'oggetto prototipo presente in mark.__proto__. Il costruttore WorkerBee assegna un valore locale per la proprietà projects. L'ereditarietà di proprietà e valori in JavaScript fnziona in questo modo. Alcune sottigliezze su questo processo sono trattate nella sezione Ereditare le proprietà (revisited).

Poiché questi costruttori non permettono di fornire valori specifici per una singola istanza, questa informazione è generica. I valori delle proprietà sono quelli di default condivisi da tutti i nuovi oggetti creati da WorkerBee. È oviamente possibile cambiare i valori di ognuna di queste proprietà. È quindi possibile assegnare informazioni specifice per mark nel modo seguente:

mark.name = "Doe, Mark";
mark.dept = "admin";
mark.projects = ["navigator"];

Aggiungere proprietà

In JavaScript, è possibile aggiungere proprietà a qualsiasi oggetto in fase di esecuzione. Non si è costretti ad usare solo le proprietà fornite dalla funzione costruttore. Per aggiungere una proprietà che sia specifica per un singolo oggetto, si deve assegnare il valore all'oggetto nel modo seguente::

mark.bonus = 3000;

Ora, l'oggetto markha una proprietà bonus, ma nessun altro WorkerBee ha questa proprietà.

Se si aggiunge una nuova proprietà a un oggetto che viene usato come prototipo per una funzione costruttore, la proprietà sarà aggiunta a tutti gli oggetti che ereditano le proprietà dal prototipo. Per esempio, è possibile aggiungere la proprietà specialty a tutti i dipendenti con l'istruzione seguente:

Employee.prototype.specialty = "none";

Non appena JavaScript esegue questa istruzione, anche l'oggetto mark avrà la proprietà specialty con il valore "none". La figura seguente mostra cosa succede qunado questa proprietà viene aggiunta al prototipo Employee e in seguito la si sovrascrive per il prototipo Engineer.


Aggiungere le proprietà

Costruttori più flessibili

Le funzioni costruttore mostrate finora non permettono di specificare i valori delle proprietà qunado viene creata un'istanza. Come in Java, è possibile fornire argomenti al costruttore per inizializzare i valori delle proprietà per le istanze. La figura seguente mostra un modo per farlo.


Specificare le proprietà in un costruttore, modo 1

La tabella seguente mostra le definizioni di questi oggetti in JavaScript e in Java.

JavaScript

Java

function Employee (name, dept) {
  this.name = name || "";
  this.dept = dept || "general";
}

 

 

 

 

public class Employee {
   public String name;
   public String dept;
   public Employee () {
      this("", "general");
   }
   public Employee (String name) {
      this(name, "general");
   }
   public Employee (String name, String dept) {
      this.name = name;
      this.dept = dept;
   }
}
function WorkerBee (projs) {
 
 this.projects = projs || [];
}
WorkerBee.prototype = new Employee;

 

 

public class WorkerBee extends Employee {
   public String[] projects;
   public WorkerBee () {
      this(new String[0]);
   }
   public WorkerBee (String[] projs) {
      projects = projs;
   }
}
 
function Engineer (mach) {
   this.dept = "engineering";
   this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;

 

 

public class Engineer extends WorkerBee {
   public String machine;
   public Engineer () {
      dept = "engineering";
      machine = "";
   }
   public Engineer (String mach) {
      dept = "engineering";
      machine = mach;
   }
}

Queste definizioni JavaScript usano un linguaggio speciale per impostare i valori di default:

this.name = name || "";

In JavaScript l'operatore logico OR (||) valuta il suo primo argomento. Se questa espressione può essere convertita a true, l'operatore restituisce il primo argomento. Altrimenti l'operatore restituisce il valore del secondo argomento. Quindi, questa linea di codice verifica se name ha un valore utile per la proprietà name. Se ce l'ha, imposta this.name a questo valore. Altrimenti, imposta this.name a una stringa vuota. Questo capitolo usa questo linguaggio per brevità, comunque può disorientare a prima vista.

Nota: Potrebbe non funzionare come atteso se la funzione costruttore è chiamata con un argomento che converte a false (come ad esempio 0 (zero) o una stringa vuota ("")). In questo caso verrà scelto il valore di default.

Con queste definizioni, quando si crea un'istanza di un oggetto, è possibile specificare i valori per le proprietà definite localmente. Per creare un nuovo Engineer è possibile utilizzare l'espressione seguente:

var jane = new Engineer("belau");

Le proprietà di Jane sono ora:

jane.name == "";
jane.dept == "engineering";
jane.projects == [];
jane.machine == "belau"

È da notare che con queste definizioni non è possibile specificare una valore iniziale per le proprietà ereditate, come ad esempio name. Se si desidera specificare un valore iniziale per una proprietà ereditata, in JavaScript è necessario aggiungere ulteriore codice alla funzione costruttore.

Finora, la funzione costruttore ha creato un oggetto generico e poi ha specificato proprietà locali e valori per il nuovo oggetto. È possibile fare in modo che il costruttore aggiunga più proprietà chiamando direttamente la funzione costruttore per un oggetto che si trova a un livello più alto nella catena dei prototipi. La figura seguente mostra queste nuove definizioni.


Specificare le proprietà in un costruttore, modo 2

Guardiamo nel dettaglio una di queste definizioni. Ecco la nuova definizione per il costruttore Engineer:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}

Supponiamo di creare un nuovo oggetto Engineer nel modo seguente:

var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");

JavaScript segue questa procedura:

  1. L'operatore new crea un oggetto generico e imposta il valore Engineer.prototype per la sua proprietà __proto__.
  2. L'operatore new passa il nuovo oggetto come valore della parola chiave this nella funzione costruttore Engineer.
  3. Il costruttore crea una nuova proprietà per l'oggetto chiamata base e assegna il valore del costruttore WorkerBee alla proprietà base. Questo rende il costruttore WorkerBee un metode dell'oggetto Engineer. Il nome della proprietà base non è peculiare. È possibile usare qualsiasi nome per la proprietà; base è semplicemente evocativo dello scopo.
  4. Il costruttore chiama il metodo base, passando come suoi argomenti due degli argomenti passati al costruttore ("Doe, Jane" e ["navigator", "javascript"]) e anche la stringa "engineering". Usando esplicitamente "engineering" nel costruttore indica che tutti gli oggetti Engineer hanno lo stesso valore per la proprietà dept ereditata, e questo valore sovrascrive il valore ereditato dal costruttore Employee.
  5. Poiché base è un metodo di Engineer, all'interno della chiamata di base, JavaScript aggiunge la parola chiave this all'oggetto creato al passaggio 1. In questo modo, la funzione WorkerBee a sua volta passa gli argomenti "Doe, Jane" e "engineering" alla funzione costruttore Employee. Dopo l'esecuzione della funzione costruttore Employee, la funzione WorkerBee utilizza l'argomento rimanente per impostare la proprietà projects.
  6. Dopo l'esecuzione del metodo base, il costruttore Engineer inizializza la proprietà machine dell'oggetto al valore "belau".
  7. Dopo l'esecuzione del costruttore, JavaScript assegna il nuovo oggetto alla variabile jane.

Avendo chiamato il costruttore WorkerBee dall'interno del costruttore Engineer, si potrebbe pensare di aver impostato in modo corretto l'ereditarietà per gli oggetti Engineer. Questo non è il caso. Chiamare il costruttore WorkerBee assicura che un oggetto Engineer venga creato con le proprietà specificate in tutte le funzioni costruttore che sono chiamate. Però, se in un secondo momento vengono aggiunte proprietà ai prototipi Employee o WorkerBee, queste proprietà non saranno ereditate dall'oggetto Engineer. Per esempio, se si considera il codice seguente:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";

L'oggetto jane non eredita la proprietà specialty. È comunque necessario impostare esplicitamente il prototipo per garantire l'ereditarietà dinamica. Se si considera invece l'esempio seguente:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";

Adesso il valore per l'oggetto jane della proprietà specialty è "none".

Un altro modo per ereditare le proprietà è l'utilizzo dei metodi call() e apply(). Gli esempi sottostanti si equivalgono:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
function Engineer (name, projs, mach) {
  WorkerBee.call(this, name, "engineering", projs);
  this.machine = mach || "";
}

L'utilizzo del metodo call() costituisce un'implementazione più pulita poiché la proprietà base non è più necessaria.

Ereditare le proprietà (revisited)

Le sezioni precedenti descrivono come i costruttori e i prototipi consentono di avere gerarchia ed ereditarietà in JavaScript. Questa sezione espone alcune sottiglienzze che non erano necessariamente evidenti nelle discussioni precedenti.

Valori locali vs. valori ereditati

Quando si accede a una proprietà di un oggetto, JavaScript esegue i seguenti passaggi, come descritto in precedenza in questo capitolo:

  1. Verifica se il valore è presente a livello locale. Se c'è, restituisce quel valore.
  2. Se non è presente, verifica la catena dei prototipi (usando la proprietà __proto__).
  3. Se un oggetto nella catena dei prototipi ha un valore per la proprietà specificata, restituisce quel valore.
  4. Se la proprietà non viene trovata, l'oggetto non ha la proprietà.

Il risultato di questo processo dipende da come sono stati definiti gli elementi. L'esempio iniziale aveva queste definizioni:

function Employee () {
  this.name = "";
  this.dept = "general";
}

function WorkerBee () {
  this.projects = [];
}
WorkerBee.prototype = new Employee;

Con queste definizioni, si supponga di creare amy come un'istanza di WorkerBee con la seguente istruzione:

var amy = new WorkerBee;

L'oggetto amy ha una proprietà locale, projects. I valori per le proprietà name e dept non sono specifici per amy e quindi derivano dalla proprietà __proto__ dell'oggetto amy. Quindi, amy ha i seguenti valori:

amy.name == "";
amy.dept == "general";
amy.projects == [];

Ora si supponga di cambiare il valore della proprietà name nel prototipo associato con Employee:

Employee.prototype.name = "Unknown"

Ci si potrebbe aspettare che il nuovo valore si propaghi a tutte le istanze di Employee. Invece, non lo fa.

Quando si crea qualsiasi istanza dell'oggetto Employee, questa istanza riceve un valore locale per la proprietà name (la stringa vuota). Questo significa che quando si imposta il prototipo WorkerBee creando un nuovo oggetto Employee, WorkerBee.prototype avrà un valore locale per la proprietà name. Quindi, quando JavaScript legge la proprietà name dell'oggetto amy (istanza di WorkerBee), trova in WorkerBee.prototype il valore locale di questa proprietà. Pertanto non continua a cercare nella catena salendo a Employee.prototype.

Se si vuole cambiare il valore di una proprietà di un oggetto in fase di esecuzione e si vuole che il nuovo valore venga ereditato da tutti i discendenti dell'oggetto, non è possibile definire la proprietà all'interno della funzione costruttore dell'oggetto. Invece, si aggiunge la proprietà al prototipo associato al costruttore. Per esempio, assumiamo di cambiare il codice precedente con quello che segue:

function Employee () {
  this.dept = "general";    // Note that this.name (a local variable) does not appear here
}
Employee.prototype.name = "";    // A single copy

function WorkerBee () {
  this.projects = [];
}
WorkerBee.prototype = new Employee;

var amy = new WorkerBee;

Employee.prototype.name = "Unknown";

In questo caso, il valore della proprietà name di amy diventa "Unknown".

Come mostra questo esempio, se si vogliono avere i valori di default per le proprietà dell'oggetto e si vuole avere la possibilità di cambiare questi valori in fase di esecuzione, si devono impostare le proprietà nel prototipo del costruttore, e non direttamente nella funzione costruttore.

Determinazione delle relazioni tra istanze

In JavaScript la ricerca delle proprietà (property lookup) controlla prima tra le proprietà specifiche dell'oggetto e, se il nome della proprietà non viene trovato, controlla la proprietà speciale __proto__. Il processo, chiamato "ricerca nella catena dei prototipi" (lookup in the prototype chain), continua ricorsivamente .

Quando un oggetto viene costruito, la proprietà speciale __proto__ viene impostata al valore della proprietà prototype del costruttore. L'espressione new Foo() crea un oggetto con __proto__ == Foo.prototype. Di conseguenza, le modifiche alle proprietà di Foo.prototype alterano la ricerca delle proprietà per tutti gli oggetti che sono stati creati con new Foo().

Ogni oggetto ha una proprietà __proto__ (ad eccezione di Object); ogni funzione ha una proprietà prototype. Quindi gli oggetti possono essere correlati ad altri oggetti attraverso una 'ereditarietà prototipale' (prototypal inheritance). È possibile verificare l'ereditarietà confrontando la proprietà __proto__ di un oggetto con l'oggetto prototype di una funzione. JavaScript fornisce una scorciatoia: l'operatore instanceof confronta un oggetto con una funzione e restituisce true se l'oggetto eredita dal prototipo della funzione. Per esempio,

var f = new Foo();
var isTrue = (f instanceof Foo);

Per un esempio più dettagliato, supponiamo di avere lo stesso gruppo di definizioni visto nella sezione For a more detailed example, suppose you have the same set of definitions shown in Ereditare le proprietà. Creiamo un oggetto Engineer nel modo seguente:

var chris = new Engineer("Pigman, Chris", ["jsd"], "fiji");

Per questo oggetto, tutti gli enunciati seguenti sono veri:

chris.__proto__ == Engineer.prototype;
chris.__proto__.__proto__ == WorkerBee.prototype;
chris.__proto__.__proto__.__proto__ == Employee.prototype;
chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype;
chris.__proto__.__proto__.__proto__.__proto__.__proto__ == null;

Dato ciò, è possibile scrivere una funzione instanceOf come segue:

function instanceOf(object, constructor) {
   object = object.__proto__;
   while (object != null) {
      if (object == constructor.prototype)
         return true;
      if (typeof object == 'xml') {
        return constructor.prototype == XML.prototype;
      }
      object = object.__proto__;
   }
   return false;
}

Nota: L'implementazione vista sopra verifica se l'oggetto è di tipo "xml" per ovviare a una stranezza nel modo in cui gli oggetti XML sono rappresentati nelle versioni recenti di JavaScript. Per i dettagli essenziali si veda il bug 634150.

Se si utilizza la funzione instanceOf definita in precedenza, queste espressioni sono vere:

instanceOf (chris, Engineer)
instanceOf (chris, WorkerBee)
instanceOf (chris, Employee)
instanceOf (chris, Object)

Ma l'espressione seguente è falsa:

instanceOf (chris, SalesPerson)

Informazioni globali nei costruttori

Quando vengono creati dei costruttori, è necessario essere scrupolosi se si impostano informazioni globali all'interno del costruttore. Per esempio, se si vuole che un ID univoco venga assegnato automaticamente a ogni nuovo Employee si potrebbe usare la definizione seguente:

var idCounter = 1;

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   this.id = idCounter++;
}

Con questa definizione, quando viene creato un nuovo Employee, il costruttore assegna l'ID seguente e incrementa il contatore globale (idCounter). Così, se l'istruzione successiva è la seguente, l'ID di victoria sarà 1, l'ID di harry sarà 2:

var victoria = new Employee("Pigbert, Victoria", "pubs")
var harry = new Employee("Tschopik, Harry", "sales")

Questa a prima vista potrebbe sembrare la procedura corretta. Tuttavia, il contatore globale idCounter viene incrementato ogni volta che viene creato un oggetto Employee, per qualsiasi scopo. Se viene creata l'intera gerarchia di oggetti Employee mostrata in questo capitolo, il costruttore Employee viene chiamato ogni volta che si definisce un prototipo. Supponiamo di avere il codice seguente:

var idCounter = 1;

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   this.id = idCounter++;
}

function Manager (name, dept, reports) {...}
Manager.prototype = new Employee;

function WorkerBee (name, dept, projs) {...}
WorkerBee.prototype = new Employee;

function Engineer (name, projs, mach) {...}
Engineer.prototype = new WorkerBee;

function SalesPerson (name, projs, quota) {...}
SalesPerson.prototype = new WorkerBee;

var mac = new Engineer("Wood, Mac");

Si assuma inoltre che le definizioni omesse abbiano la proprietà base e chiamino il costruttore che si trova al livello superiore nella catena dei prototipi. In questo caso, nel momento in cui viene creato l'oggetto mac, il valore di mac.id sarà 5.

A seconda dell'applicazione, può essere più o meno importante che il valore del contatore sia stato incrementato queste volte aggiuntive. Se interessa il valore esatto di questo contatore, una soluzione possibile può prevedere l'uso del costruttore seguente al posto di quello visto in precedenza:

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   if (name)
      this.id = idCounter++;
}

Quando viene creata un'istanza di Employee che deve essere usata come prototipo, non vengono forniti argomenti al costruttore. Usando questa definizione del costruttore, quando non vengono inseriti argomenti, il costruttore non assegna un valore all'ID e non aggiorna il contatore. Quindi, affinché a un oggetto Employee venga assegnato un ID, è necesario specificare un nome per il dipendente. In questo esempio, l'ID di mac sarà 1.

JavaScript non supporta l'ereditarietà multipla

Alcuni linguaggi ad oggetti ammettono l'ereditarietà multipla. Ossia, un oggetto può ereditare proprietà e valori da oggetti genitori non correlati. Javascript non supporta l'ereditarietà multipla.

L'eredità dei valori delle proprietà si ha in fase di esecuzione quando JavaScript cerca attraverso la catena dei prototipo di un oggetto per trovare un valore. Poiché un oggetto ha un unico prototipo associato, JavaScript non può ereditare dinamicamente da più di una catena di prototipi.

In JavaScript, è possibile che una funzione costruttore chiami al suo interno diverse funzioni costruttore. Questo dà l'illusione dell'ereditarietà multipla. Per esempio consideriamo le istruzioni seguenti:

function Hobbyist (hobby) {
   this.hobby = hobby || "scuba";
}

function Engineer (name, projs, mach, hobby) {
   this.base1 = WorkerBee;
   this.base1(name, "engineering", projs);
   this.base2 = Hobbyist;
   this.base2(hobby);
   this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;

var dennis = new Engineer("Doe, Dennis", ["collabra"], "hugo")

Assumiamo inoltre che la definizione di WorkerBee sia quella usata in precedenza in questo capitolo. In questo caso, l'oggetto dennis avrà queste proprietà:

dennis.name == "Doe, Dennis"
dennis.dept == "engineering"
dennis.projects == ["collabra"]
dennis.machine == "hugo"
dennis.hobby == "scuba"

Quindi dennis riceve la proprietà hobby dal costruttore Hobbyist. Però, se in seguito si aggiunge una proprietà al prototipo del costruttore Hobbyist:

Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]

L'oggetto dennis non erediterà questa nuova proprietà.

Tag del documento e collaboratori

 Hanno collaborato alla realizzazione di questa pagina: dem-s
 Ultima modifica di: dem-s,