Ci sono due tipi di proprietà di oggetti.
Il primo tipo sono le proprietà dati. Sappiamo già come lavorare con loro. Tutte le proprietà che abbiamo usato finora erano proprietà dati.
Il secondo tipo di proprietà è qualcosa di nuovo. Sono le proprietà accessorie. Sono essenzialmente funzioni che eseguono per ottenere e impostare un valore, ma sembrano normali proprietà per un codice esterno.
Getters e setters
Le proprietà accessorie sono rappresentate da metodi “getter” e “setter”. In un letterale di oggetto sono indicati da get
e set
:
let obj = { get propName() { // getter, the code executed on getting obj.propName }, set propName(value) { // setter, the code executed on setting obj.propName = value }};
Il getter funziona quando obj.propName
viene letto, il setter – quando viene assegnato.
Per esempio, abbiamo un oggetto user
con name
e surname
:
let user = { name: "John", surname: "Smith"};
Ora vogliamo aggiungere una proprietà fullName
, che dovrebbe essere "John Smith"
. Naturalmente, non vogliamo fare un copia-incolla delle informazioni esistenti, quindi possiamo implementarlo come un accessor:
let user = { name: "John", surname: "Smith", get fullName() { return `${this.name} ${this.surname}`; }};alert(user.fullName); // John Smith
Dall’esterno, una proprietà accessor sembra una proprietà normale. Questa è l’idea delle proprietà accessorie. Non chiamiamo user.fullName
come una funzione, la leggiamo normalmente: il getter gira dietro le quinte.
A partire da ora, fullName
ha solo un getter. Se tentiamo di assegnare user.fullName=
, ci sarà un errore:
let user = { get fullName() { return `...`; }};user.fullName = "Test"; // Error (property has only a getter)
Correggiamolo aggiungendo un setter per user.fullName
:
let user = { name: "John", surname: "Smith", get fullName() { return `${this.name} ${this.surname}`; }, set fullName(value) { = value.split(" "); }};// set fullName is executed with the given value.user.fullName = "Alice Cooper";alert(user.name); // Alicealert(user.surname); // Cooper
Come risultato, abbiamo una proprietà “virtuale” fullName
. È leggibile e scrivibile.
Descrittori degli accessi
I descrittori delle proprietà degli accessi sono diversi da quelli delle proprietà dei dati.
Per le proprietà degli accessi, non c’è value
o writable
, ma ci sono invece get
e set
funzioni.
Ovvero, un descrittore di accesso può avere:
-
get
– una funzione senza argomenti, che funziona quando una proprietà viene letta, -
set
– una funzione con un argomento, che viene chiamata quando la proprietà viene impostata, -
enumerable
– come per le proprietà dati, -
configurable
– come per le proprietà dati.
Per esempio, per creare un accessor fullName
con defineProperty
, possiamo passare un descrittore con get
e set
:
let user = { name: "John", surname: "Smith"};Object.defineProperty(user, 'fullName', { get() { return `${this.name} ${this.surname}`; }, set(value) { = value.split(" "); }});alert(user.fullName); // John Smithfor(let key in user) alert(key); // name, surname
Si noti che una proprietà può essere o un accessor (ha get/set
metodi) o una proprietà dati (ha un value
), non entrambi.
Se proviamo a fornire sia get
che value
nello stesso descrittore, ci sarà un errore:
// Error: Invalid property descriptor.Object.defineProperty({}, 'prop', { get() { return 1 }, value: 2});
Getters/setters più intelligenti
Getters/setters possono essere usati come wrapper sui valori di proprietà “reali” per ottenere più controllo sulle operazioni con essi.
Per esempio, se vogliamo proibire nomi troppo brevi per user
, possiamo avere un setter name
e mantenere il valore in una proprietà separata _name
:
let user = { get name() { return this._name; }, set name(value) { if (value.length < 4) { alert("Name is too short, need at least 4 characters"); return; } this._name = value; }};user.name = "Pete";alert(user.name); // Peteuser.name = ""; // Name is too short...
Così, il nome è memorizzato nella proprietà _name
e l’accesso è fatto tramite getter e setter.
Tecnicamente, il codice esterno può accedere direttamente al nome usando user._name
. Ma c’è una convenzione ampiamente conosciuta che le proprietà che iniziano con un underscore "_"
sono interne e non dovrebbero essere toccate dall’esterno dell’oggetto.
Usare per compatibilità
Uno dei grandi usi degli accessori è che permettono di prendere il controllo di una proprietà di dati “regolare” in qualsiasi momento sostituendola con un getter e un setter e modificare il suo comportamento.
Immaginate di iniziare a implementare gli oggetti utente usando le proprietà dati name
e age
:
function User(name, age) { this.name = name; this.age = age;}let john = new User("John", 25);alert( john.age ); // 25
…Ma prima o poi, le cose potrebbero cambiare. Invece di age
potremmo decidere di memorizzare birthday
, perché è più preciso e conveniente:
function User(name, birthday) { this.name = name; this.birthday = birthday;}let john = new User("John", new Date(1992, 6, 1));
Ora cosa fare con il vecchio codice che usa ancora la proprietà age
?
Possiamo cercare di trovare tutti questi posti e sistemarli, ma questo richiede tempo e può essere difficile da fare se quel codice è usato da molte altre persone. E poi, age
è una bella cosa da avere in user
, giusto?
Teniamolo.
Aggiungere un getter per age
risolve il problema:
function User(name, birthday) { this.name = name; this.birthday = birthday; // age is calculated from the current date and birthday Object.defineProperty(this, "age", { get() { let todayYear = new Date().getFullYear(); return todayYear - this.birthday.getFullYear(); } });}let john = new User("John", new Date(1992, 6, 1));alert( john.birthday ); // birthday is availablealert( john.age ); // ...as well as the age
Ora anche il vecchio codice funziona e abbiamo una bella proprietà aggiuntiva.