Istnieją dwa rodzaje właściwości obiektów.
Pierwszym rodzajem są właściwości danych. Wiemy już, jak z nimi pracować. Wszystkie właściwości, których używaliśmy do tej pory, były właściwościami danych.
Drugi rodzaj właściwości jest czymś nowym. Są to właściwości accessor. Są to w zasadzie funkcje, które wykonują się przy pobieraniu i ustawianiu wartości, ale dla zewnętrznego kodu wyglądają jak zwykłe właściwości.
Getters and setters
Właściwości akcesora są reprezentowane przez metody „getter” i „setter”. W literale obiektowym są one oznaczane przez get
i 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 }};
Getter działa, gdy obj.propName
jest odczytywany, setter – gdy jest przypisywany.
Na przykład mamy obiekt user
z name
i surname
:
let user = { name: "John", surname: "Smith"};
Teraz chcemy dodać właściwość fullName
, która powinna być "John Smith"
. Oczywiście nie chcemy kopiuj-wklej istniejącej informacji, więc możemy ją zaimplementować jako accessor:
let user = { name: "John", surname: "Smith", get fullName() { return `${this.name} ${this.surname}`; }};alert(user.fullName); // John Smith
Z zewnątrz właściwość accessor wygląda jak zwykła. Na tym właśnie polega idea właściwości accessor. Nie wywołujemy user.fullName
jako funkcji, tylko czytamy ją normalnie: getter działa za kulisami.
Jak na razie, fullName
ma tylko getter. Jeśli spróbujemy przypisać user.fullName=
, pojawi się błąd:
let user = { get fullName() { return `...`; }};user.fullName = "Test"; // Error (property has only a getter)
Poprawmy to, dodając setter dla 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
W rezultacie mamy „wirtualną” właściwość fullName
. Jest ona odczytywalna i zapisywalna.
Deskryptory dla właściwości accessor
Deskryptory dla właściwości accessor różnią się od deskryptorów dla właściwości data.
Dla właściwości accessor nie ma value
lub writable
, ale zamiast tego są funkcje get
i set
.
To znaczy, deskryptor dostępu może mieć:
-
get
– funkcję bez argumentów, która działa, gdy właściwość jest odczytywana, -
set
– funkcję z jednym argumentem, która jest wywoływana, gdy właściwość jest ustawiana, -
enumerable
– tak samo jak dla właściwości danych, -
configurable
– tak samo jak dla właściwości danych.
Na przykład, aby utworzyć accessor fullName
z defineProperty
, możemy przekazać deskryptor z get
i 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
Proszę zauważyć, że właściwość może być albo accessorem (ma get/set
metod), albo właściwością danych (ma value
), a nie obydwoma.
Jeśli spróbujemy dostarczyć zarówno get
, jak i value
w tym samym deskryptorze, wystąpi błąd:
// Error: Invalid property descriptor.Object.defineProperty({}, 'prop', { get() { return 1 }, value: 2});
Smarter getters/setters
Getters/setters mogą być używane jako wrappery nad „prawdziwymi” wartościami właściwości, aby uzyskać większą kontrolę nad operacjami z nimi.
Na przykład, jeśli chcemy zabronić zbyt krótkich nazw dla user
, możemy mieć setter name
i przechowywać wartość w oddzielnej właściwości _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...
Więc, nazwa jest przechowywana we właściwości _name
, a dostęp do niej odbywa się poprzez getter i setter.
Technicznie, kod zewnętrzny jest w stanie uzyskać bezpośredni dostęp do nazwy za pomocą user._name
. Ale istnieje powszechnie znana konwencja, że właściwości zaczynające się od podkreślnika "_"
są wewnętrzne i nie powinny być dotykane spoza obiektu.
Użycie dla kompatybilności
Jednym z wielkich zastosowań accessorów jest to, że pozwalają one w każdej chwili przejąć kontrolę nad „zwykłą” właściwością danych poprzez zastąpienie jej getterem i setterem i podrasowanie jej zachowania.
Wyobraźmy sobie, że zaczęliśmy implementować obiekty użytkownika używając właściwości danych name
i age
:
function User(name, age) { this.name = name; this.age = age;}let john = new User("John", 25);alert( john.age ); // 25
…Ale prędzej czy później wszystko może się zmienić. Zamiast age
możemy zdecydować się na przechowywanie birthday
, ponieważ jest to bardziej precyzyjne i wygodne:
function User(name, birthday) { this.name = name; this.birthday = birthday;}let john = new User("John", new Date(1992, 6, 1));
A teraz co zrobić ze starym kodem, który nadal używa właściwości age
?
Możemy spróbować znaleźć wszystkie takie miejsca i je naprawić, ale to wymaga czasu i może być trudne do wykonania, jeśli ten kod jest używany przez wiele innych osób. A poza tym age
jest miłą rzeczą, którą warto mieć w user
, prawda?
Zachowajmy ją.
Dodanie gettera dla age
rozwiązuje problem:
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
Teraz stary kod też działa, a my mamy fajną dodatkową właściwość.