Det finns två typer av objektegenskaper.
Den första typen är dataegenskaper. Vi vet redan hur man arbetar med dem. Alla egenskaper som vi hittills har använt var dataegenskaper.
Den andra typen av egenskaper är något nytt. Det är accessoregenskaper. De är i huvudsak funktioner som exekverar vid hämtning och inställning av ett värde, men ser ut som vanliga egenskaper för en extern kod.
Getters och setters
Accessor properties representeras av ”getter”- och ”setter”-metoder. I en objektlitteratur betecknas de av get
och 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 }};
Gettern fungerar när obj.propName
läses, settern – när den tilldelas.
Till exempel har vi ett user
objekt med name
och surname
:
let user = { name: "John", surname: "Smith"};
Nu vill vi lägga till en fullName
egenskap, som ska vara "John Smith"
. Naturligtvis vill vi inte kopiera-klistra in befintlig information, så vi kan implementera den som en accessor:
let user = { name: "John", surname: "Smith", get fullName() { return `${this.name} ${this.surname}`; }};alert(user.fullName); // John Smith
Från utsidan ser en accessor-egenskap ut som en vanlig egenskap. Det är tanken med accessor properties. Vi anropar inte user.fullName
som en funktion, vi läser den normalt: gettern körs bakom kulisserna.
Från och med nu har fullName
bara en getter. Om vi försöker tilldela user.fullName=
blir det ett fel:
let user = { get fullName() { return `...`; }};user.fullName = "Test"; // Error (property has only a getter)
Låt oss åtgärda det genom att lägga till en setter för 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
Som resultat har vi en ”virtuell” egenskap fullName
. Den är läsbar och skrivbar.
Accessor deskriptorer
Deskriptorer för accessoregenskaper skiljer sig från dem för dataegenskaper.
För accessoregenskaper finns det ingen value
eller writable
, utan istället finns det get
och set
funktioner.
Det vill säga, en accessorbeskrivare kan ha:
-
get
– en funktion utan argument, som fungerar när en egenskap läses, -
set
– en funktion med ett argument, som anropas när egenskapen sätts, -
enumerable
– samma som för dataegenskaper, -
configurable
– samma som för dataegenskaper.
För att till exempel skapa en accessor fullName
med defineProperty
kan vi skicka en deskriptor med get
och 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
Observera att en egenskap kan vara antingen en accessor (har get/set
metoder) eller en dataegenskap (har en value
), inte båda.
Om vi försöker tillhandahålla både get
och value
i samma deskriptor kommer det att uppstå ett fel:
// Error: Invalid property descriptor.Object.defineProperty({}, 'prop', { get() { return 1 }, value: 2});
Smarta getters/setters
Getters/setters kan användas som omslag över ”riktiga” egenskapsvärden för att få mer kontroll över operationer med dem.
Om vi till exempel vill förbjuda alltför korta namn för user
kan vi ha en setter name
och behålla värdet i en separat egenskap _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...
Namnet lagras alltså i egenskap _name
och åtkomsten sker via getter och setter.
Tekniskt sett kan extern kod få direkt åtkomst till namnet genom att använda user._name
. Men det finns en allmänt känd konvention om att egenskaper som börjar med ett understreck "_"
är interna och inte bör beröras från utsidan av objektet.
Användning för kompatibilitet
En av de stora användningsområdena för accessorerna är att de gör det möjligt att ta kontroll över en ”vanlig” dataegenskap när som helst genom att ersätta den med en getter och en setter och justera dess beteende.
Föreställ dig att vi började implementera användarobjekt med hjälp av dataegenskaper name
och age
:
function User(name, age) { this.name = name; this.age = age;}let john = new User("John", 25);alert( john.age ); // 25
…Men förr eller senare kan saker och ting förändras. Istället för age
kanske vi bestämmer oss för att lagra birthday
, eftersom det är mer exakt och bekvämt:
function User(name, birthday) { this.name = name; this.birthday = birthday;}let john = new User("John", new Date(1992, 6, 1));
Vad ska vi nu göra med den gamla koden som fortfarande använder age
-egenskapen?
Vi kan försöka hitta alla sådana ställen och rätta till dem, men det tar tid och kan vara svårt att göra om den koden används av många andra människor. Dessutom är age
en trevlig sak att ha i user
, eller hur?
Låt oss behålla den.
Att lägga till en getter för age
löser problemet:
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
Nu fungerar den gamla koden också och vi har en trevlig extra egenskap.