Una alternativa ai metodi visti finora per la creazione di oggetti è quella di fare ricorso al metodo create() dell'oggetto base Object. Questo metodo ci consente sia di mantenere la flessibilità tipica degli oggetti JavaScript sia di aggiungere un ulteriore livello di espressività nella definizione di un oggetto. Ma vediamo come utilizzarlo mediante alcuni esempi.
Nella sua forma minima il metodo create()
consente di creare un oggetto specificando il prototipo di riferimento:
var persona = Object.create(null);
In questo esempio abbiamo specificato null
come prototipo di riferimento dell'oggetto persona
che abbiamo creato. Il risultato è un oggetto senza proprietà né metodi, nemmeno quelli ereditati dall'oggetto base Object
, come avremmo ottenuto creando un oggetto vuoto tramite notazione letterale ({}
). Per ottenere un oggetto equivalente all'oggetto vuoto dobbiamo scrivere il seguente codice:
var persona = Object.create(Object.prototype);
Con esso specifichiamo di voler creare un oggetto il cui prototipo è il prototipo di Object.
Naturalmente il principio vale per qualsiasi oggetto. Riconsideriamo quindi la nostra definizione dell'oggetto persona
definendo opportunamente il suo prototipo:
function persona(nome, cognome) {
this.nome = (nome || "");
this.cognome = (cognome ||"");
}
persona.prototype.indirizzo = "";
persona.prototype.email = "";
persona.prototype.mostraNomeCompleto = function() {return this.nome + " " + this.cognome};
persona.prototype.calcolaCodiceFiscale = function() {...};
Abbiamo mantenuto all'interno del costruttore le proprietà che possono essere inizializzate in fase di creazione dell'oggetto e spostato le altre proprietà e metodi sul relativo prototipo.
Possiamo utilizzare il metodo create()
per creare un oggetto nel seguente modo:
var marioRossi = Object.create(persona.prototype);
Naturalmente in questo caso abbiamo creato un oggetto senza utilizzare il costruttore e quindi non abbiamo impostato direttamente le due proprietà nome
e cognome
. Il fatto è che, avendo tenuto le due proprietà fuori dalla definizione del prototipo, esse non sono presenti nella struttura dell'oggetto marioRossi
. Possiamo rendercene conto invocando il metodo mostraNomeCompleto()
, che restituirà un undefined undefined
invece che una stringa vuota come ci aspetteremmo.
L'inizializzazione di un oggetto creato con Object.create()
avviene tramite il secondo parametro opzionale del metodo:
var marioRossi = Object.create(
persona.prototype,
{
nome: { value: "Mario" },
cognome: { value: "Rossi" }
});
Questo assegnamento crea un l'oggetto marioRossi
basato sul prototipo persona
e con le proprietà nome e cognome inizializzate.
A prima vista l'inizializzazione delle proprietà appare una cosa un po' più complessa di quanto probabilmente avremmo immaginato. In realtà l'inizializzazione di un oggetto tramite il metodo create()
offre un'opportunità molto interessante nella definizione di oggetti in JavaScript, cioè l'uso di descrittori delle proprietà.
I descrittori delle proprietà
Un descrittore è un oggetto che definisce caratteristiche e modalità di accesso alle proprietà di un oggetto. Possiamo distinguere due tipi di descrittori:
- data descriptor, un oggetto che definisce una proprietà specificando una serie di caratteristiche predefinite;
- accessor descriptor, descrive una proprietà tramite una coppia di funzioni di tipo
getter
esetter
.
Consideriamo il seguente esempio:
var marioRossi = Object.create(
persona.prototype, {
nome: {
value : "Mario",
writable : false,
configurable : false },
cognome: {
value : "Rossi",
writable : false,
configurable : false },
indirizzo: {
value : "",
writable : true,
configurable : true },
email: {
value: "", writable: true, configurable: true},
nomeCompleto: {
configurable: true,
get: function() {return this.nome + " " + this.cognome;}
}
});
Abbiamo definito ed inizializzato l'oggetto marioRossi
specificando che le proprietà nome
e cognome
non possono essere modificate nè rimosse, mentre le proprietà indirizzo
ed email
possono essere sia modificate che rimosse o ridefinite. Inoltre abbiamo definito la proprietà in sola lettura nomeCompleto
impostando la funzione che fa da getter
.
Come possiamo vedere abbiamo un insieme di opzioni per definire come è possibile accedere alle proprietà del nostro oggetto. Andando nello specifico, quelle che seguono sono le opzioni disponibili per la definizione di una proprietà tramite un data descriptor
:
writable | Booleano che indica se il valore della proprietà può essere modificato |
configurable | Booleano che indica se il tipo di descrittore può essere modificato e se la proprietà può essere rimossa |
enumerable | Booleano che indica se la proprietà è accessibile durante un ciclo sulle proprietà dell'oggetto |
value | Indica il valore della proprietà |
Le opzioni disponibili per la definizione di una proprietà tramite un accessor descriptor
sono:
configurable | Booleano che indica se il tipo di descrittore può essere modificato e se la proprietà può essere rimossa |
enumerable | Booleano che indica se la proprietà è accessibile durante un ciclo sulle proprietà dell'oggetto |
get | Funzione senza argomenti invocata quando si accede alla proprietà in lettura |
set | Funzione chiamata quando si accede alla proprietà in scrittura; il nuovo valore da assegnare alla proprietà viene passato come parametro |
Per comprendere come sfruttare gli accessor descriptor, vediamo come definire ad esempio la proprietà email
se vogliamo una validazione dei valori che possono essere assegnati:
_email: { value: "", writable: true, configurable: true },
email: {
get: function() {
return this._email;
},
set: function(value) {
var emailRegExp = /\w+@\w+\.\w{2,4}/i;
if (emailRegExp.test(value)) {
this._email = value;
} else {
console.log("Email non valida!");
}
}
}
Facciamo ricorso in questo caso a due proprietà: la prima, _email
, intesa per contenere il valore e la seconda, email
, definita tramite get
e set
per gestire l'accesso al valore della proprietà. Nel caso specifico, l'opzione set
dell'accessor descriptor
verifica che il valore da assegnare sia effettivamente un'indirizzo di email formalmente valido.
Possiamo utilizzare il metodo Object.create()
per creare il costruttore di un oggetto derivato. Ad esempio, se vogliamo creare il costruttore per un oggetto programmatore
derivato da persona
possiamo procedere come mostrato di seguito:
function programmatore(nome, cognome)
{
persona.call(this, nome, cognome);
this.linguaggiConosciuti = [];
}
programmatore.prototype = Object.create(persona.prototype);
Come possiamo vedere, abbiamo invocato la funzione che definisce il costruttore di persona tramite il metodo call()
in modo tale da mappare
un'istanza dell'oggetto persona
sull'oggetto programmatore
. In pratica, l'effetto che otteniamo è l'esecuzione del corpo della funzione persona()
all'interno della funzione programmatore()
, riportando di fatto su programmatore
le inizializzazioni previste per persona
. Aggiungiamo quindi a programmatore
una nuova proprietà linguaggiConosciuti
. Infine, tramite Object.create()
, impostiamo il prototipo di programmatore
facendolo puntare a quello di persona
.