Per creare oggetti in Javascript, oltre alle funzioni costruttore ed all'operatore new
, possiamo sfruttare il metodo Object.create()
. Abbiamo parlato di questo aspetto in un'apposita lezione della guida a Javascript di HTML.it. In questo approfondimento, vedremo alcune dinamiche in maggiore dettaglio.
Sintassi di Object.create
L'API di Object.create()
è alquanto semplice:
Object.create(prototype_object, propertiesObject)
Il primo parametro rappresenta l'oggetto che fungerà da prototipo, definendone quindi la struttura. Se viene passato null
, verrà generato un oggetto vuoto, senza una struttura precisa. Il secondo parametro (opzionale) è invece contenente le proprietà ed i metodi del neo oggetto. Il valore di ritorno è di tipo object
, e rappresenta il nuovo oggetto con il prototipo indicato e le proprietà assegnate.
Se il secondo parametro è impostato a null
, verrà generata un'eccezione di tipo TypeError
.
Utilizzando come oggetto prototipo Object.prototype
, la dichiarazione di un oggetto è equivalente alla dichiarazione letterale:
o = {};
// uguale a:
o = Object.create(Object.prototype);
Vediamo dunque un semplice utilizzo di Object.create()
:
var person = Object.create(null);
typeof(person) // Object
console.log(person) // Object il cui prototype è null
// imposta proprietà
person.name = "Riccardo";
console.log(person) // oggetto con protoype null e proprietà name
Dato che nell'oggetto appena creato abbiamo passato null
come primo argomento, questo non avrà un oggetto prototype. In seguito impostiamo una proprietà denominata name che sarà disponibile pubblicamente.
Vediamo ora un esempio in cui viene fornito anche l'oggetto prototype:
var prototypeObject = {
fullName: function(){
return this.firstName + " " + this.lastName
}
}
var person = Object.create(prototypeObject);
console.log(person); // oggetto con prototype ma senza proprietà
// aggiungiamo le proprietà
person.firstName = "Riccardo";
person.lastName = "Degni";
person.fullName(); // Riccardo Degni
In questo esempio, l'oggetto person eredita un oggetto prototype che contiene il metodo fullName
. Abbiamo inoltre aggiunto le proprietà pubbliche firstName
e lastName
, senza avvalerci della sintassi di Object.create()
. Tuttavia, è possibile includere questo tipo di operazione nella procedura iniziale di costruzione dell'oggetto, utilizzando il secondo argomento opzionale del metodo create.
Il secondo parametro è infatti utilizzato per fornire all'oggetto le sue proprietà specifiche. Agisce come un "descrittore" delle proprietà stesse. A differenza delle proprietà impostate esternamente, in questo caso possiamo definire alcune importanti opzioni di configurazione. Queste opzioni si distinguono in data descriptors e access descriptors.
I data descriptors sono:
configurable
:true
se (e solo se) il tipo di questo descrittore di proprietà può essere cambiato, e se la proprietà stessa viene cancellata dall'oggetto corrispondente. Di default èfalse
enumerable
:true
se (e solo se) questa proprietà viene mostrata nelle enumerazioni delle proprietà dell'oggetto corrispondente. Con la specifica ECMA 5, le modalità con cui queste operazioni vengono effettuate sono:- ciclo for...in
Object.keys(o)
Object.getOwnPropertyNames(o)
Di default è false
value
: il valore della proprietà. Può essere qualsiasi valore Javascript validowritable
:true
se (e solo se) il valore della proprietà può essere modificato attraverso l'operatore di assegnamento
Gli access descriptors sono:
get
: una funzione che funge da getter per la proprietà, oundefined
se non viene definito un getter. La funzione restituisce il valore della proprietà. Di default èundefined
set
: una funzione che funge da setter per la proprietà, oundefined
se non viene definito un setter. La funzione riceve come unico argomento il nuovo valore che viene assegnato alla proprietà
Andiamo dunque a creare il nostro oggetto person completo di prototipo e proprietà:
var prototypeObject = {
fullName: function(){
return this.firstName + " " + this.lastName
}
}
var person = Object.create(prototypeObject, {
'firstName': {
value: "Riccardo",
writable: true,
enumerable: true
},
'lastName': {
value: "Degni",
writable: true,
enumerable: true
}
});
console.log(person); // oggetto completo di oggetto prototype e di proprietà assegnate in fase di creazione
Getters e Setters
Nello snippet precedente abbiamo appositamente saltato due componenti fondamentali introdotti da ECMA 5, che è possibile utilizzare nella fase di costruzione di un oggetto: i getters ed i setters.
La modalità classica con cui prelevare ed impostare i valori delle proprietà, era prodotta attraverso una procedura del tipo seguente:
person.setLastName('Degni');
person.setFirstName('Riccardo');
person.getFullName(); // Riccardo Degni
Tuttavia, questo approccio ha diverse limitazioni, e non possiede natura dinamica. Vediamo invece come produrre un codice più moderno e performante avvalendoci dei getters e dei setters:
var person = {
firstName: 'Riccardo',
lastName: 'Degni',
get fullName() {
return this.firstName + ' ' + this.lastName;
},
set fullName (name) {
var words = name.toString().split(' ');
this.firstName = words[0] || '';
this.lastName = words[1] || '';
}
}
person.fullName = 'Riccardo Degni';
console.log(person.firstName); // Riccardo
console.log(person.lastName); // Degni
In questo modo, quando la proprietà fullName
viene settata, viene richiamato il setter, passando il valore che viene utilizzato. Nel nostro esempio vengono eseguite delle operazioni che permettono di produrre altre due proprietà, firstName
e lastName
. Quando invece la proprietà fullName
viene letta, il getter restituisce una stringa che è la combinazione tra le proprietà firstName
e lastName
.
Con ECMA 5 possiamo definire le proprietà anche grazie al metodo Object.defineProperty()
, che ci permette di definire le nostre proprietà in modo esteso anche dopo la creazione dell'oggetto, e soprattutto ci permette di utilizzare gli altri descrittori:
var person = {
firstName: 'Riccardo',
lastName: 'Degni'
};
Object.defineProperty(person, 'fullName', {
get: function() {
return this.firstName + ' ' + this.lastName;
},
set: function(name) {
var words = name.split(' ');
this.firstName = words[0] || '';
this.lastName = words[1] || '';
},
configurable: true, enumerable: true
});
Si noti che, a differenza di configurable
ed enumerable
, la proprietà writable
(con l'annesso valore) non è utilizzabile con i descrittori getter e setter.
Dunque, utilizziamo i getters ed i setters in fase di definizione delle proprietà, in combinazione con Object.create()
:
var prototypeObject = {
// proprietà del prototype
}
var person = Object.create(prototypeObject, {
'firstName': {
value: "Riccardo",
writable: true,
enumerable: true
},
'lastName': {
value: "Degni",
writable: true,
enumerable: true
},
'fullName': {
get() {
return this.firstName + ' ' + this.lastName;
},
set (name) {
var words = name.toString().split(' ');
this.firstName = words[0] || '';
this.lastName = words[1] || '';
}
}
});
console.log(person.fullName);
Ereditarietà
Come ultima nota conclusiva, si noti che anche con Object.create è possibile implementare l'ereditarietà classica di Javascript. Il codice seguente rappresenta un esempio di ereditarietà singola:
// Shape - superclass
function Shape() {
this.x = 0;
this.y = 0;
}
// superclass - metodo
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - subclass
function Rectangle() {
// chiama il super constructor
Shape.call(this);
}
// subclass estende superclass
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle();
// true
console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle);
// true
console.log('Is rect an instance of Shape?', rect instanceof Shape);
// Logga 'Shape moved.'
rect.move(1, 1);
Ulteriori informazioni sull'uso di Object.create() possono essere reperite nell'apposita lezione della guida a Javascript di HTML.it.