Геттери і сеттери в класі
Геттер і сеттер, це пара спеціальних методів класу, які виконуються автоматично при читанні (obj.prop) та присвоєнні (obj.prop = value). Ззовні властивість виглядає як звичайне поле, але насправді за нею стоїть код, який може валідувати, обчислювати чи логувати.
Теорія
TL;DR
get name()спрацьовує на читанніobj.name,set name(value), на присвоєнніobj.name = value.- Це accessor-властивість: значення в ній не зберігається, воно лише обчислюється або делегується в інше поле.
- Лише геттер, властивість тільки для читання; лише сеттер, властивість тільки для запису (читання дасть
undefined). - Ідеально поєднуються з приватними полями
#field: дані захищені, доступ контрольований. - Той самий механізм доступний поза класом через
Object.definePropertyабо літерал об'єкта.
Швидкий приклад
class User {
constructor(name) {
this._name = name; // «приватне» поле за домовленістю
}
get name() {
console.log('getter called');
return this._name;
}
set name(value) {
console.log('setter called');
if (value.length < 2) throw new Error('Name is too short');
this._name = value;
}
}
const u = new User('Maria');
console.log(u.name); // 'getter called', потім 'Maria'
u.name = 'Oleh'; // 'setter called'
console.log(u.name); // 'Oleh'Синтаксис виклику лишається як у звичайного поля: жодних дужок, жодного getName().
Як це працює під капотом
get і set у тілі класу, це скорочення для accessor-дескриптора властивості. Формально клас робить приблизно те саме, що й Object.defineProperty:
const user = {
_name: 'Maria'
};
Object.defineProperty(user, 'name', {
get() {
return this._name.toUpperCase();
},
set(value) {
this._name = value.trim();
}
});
user.name = ' oleh ';
console.log(user.name); // 'OLEH'Важливі наслідки:
- Аксесор кладеться на прототип класу, а не на екземпляр, тому не дублюється в кожному об'єкті.
- Він неперелічуваний (
enumerable: false), тож не потрапляє вObject.keys(instance)і в результатJSON.stringify(instance). Якщо обчислюване значення потрібне в JSON, додайте методtoJSON(). - Дескриптор можна прочитати:
Object.getOwnPropertyDescriptor(User.prototype, 'name')поверне об'єкт з полямиgetіset. - Аксесор може бути статичним:
static get version() { return '1.0'; }читається якUser.version.
Геттер без сеттера і сеттер без геттера
Якщо визначено лише геттер, властивість стає тільки для читання. Її типово використовують для обчислюваних значень: area, fullName, priceWithTax.
class Circle {
constructor(radius) {
this.radius = radius;
}
get area() {
return Math.PI * this.radius ** 2;
}
}
const c = new Circle(10);
console.log(c.area); // 314.159...
c.area = 100; // у модулі або в strict mode: TypeError
console.log(c.area); // 314.159..., значення не змінилосяУ нестрогому режимі таке присвоєння просто мовчки ігнорується, а в модулях і в strict mode кидає
TypeError: Cannot set property area. Код класів завжди строгий, але сам рядок присвоєння виконується у своєму контексті, тому поведінка залежить від нього.
Дзеркальний випадок, сеттер без геттера, зручний, коли треба лише реагувати на запис і перерахувати внутрішні дані.
class Temperature {
set celsius(value) {
this.fahrenheit = value * 1.8 + 32;
}
}
const t = new Temperature();
t.celsius = 25;
console.log(t.fahrenheit); // 77
console.log(t.celsius); // undefined, геттера немаєРобота з приватними полями
Найчастіше геттер і сеттер, це фасад над приватним полем #field. Дані недоступні ззовні, але клас сам вирішує, як їх віддавати і що приймати.
class Account {
#balance = 0;
get balance() {
return this.#balance;
}
set balance(value) {
if (value < 0) throw new Error('Balance cannot be negative');
this.#balance = value;
}
}
const acc = new Account();
acc.balance = 100;
console.log(acc.balance); // 100
// acc.balance = -50; // Error: Balance cannot be negativeТакий підхід дає головну перевагу аксесорів: публічний контракт (acc.balance) лишається стабільним, а внутрішнє зберігання можна змінити будь-коли.
Обчислювані властивості
Геттер разом із сеттером дозволяє розкласти складене значення на частини і зібрати назад.
class User {
constructor(first, last) {
this.first = first;
this.last = last;
}
get fullName() {
return `${this.first} ${this.last}`;
}
set fullName(value) {
const [first, last] = value.split(' ');
this.first = first;
this.last = last;
}
}
const u = new User('Maria', 'Koval');
console.log(u.fullName); // 'Maria Koval'
u.fullName = 'Oleh Shevchenko';
console.log(u.first); // 'Oleh'
console.log(u.last); // 'Shevchenko'Властивість fullName не існує як поле в пам'яті: вона щоразу обчислюється з first і last, тому неможливо отримати розсинхронізовані дані.
Порівняння зі звичайною властивістю
| Особливість | Звичайна властивість | Геттер або сеттер |
|---|---|---|
| Зберігає значення | так | ні, обчислює або делегує |
| Виконує код при доступі | ні | так, автоматично |
| Можна додати валідацію чи логіку | ні | так |
| Працює з приватними полями | так | так |
| Синтаксис звернення | obj.prop | obj.prop, такий самий |
| Живе на екземплярі | так | на прототипі класу |
Потрапляє в JSON.stringify | так | ні |
Типові помилки
- Викликати як метод.
u.name()кинеTypeError: u.name is not a function, бо геттер повертає значення, а не функцію. - Нескінченна рекурсія. Якщо в
set name(value)написатиthis.name = value, сеттер викличе сам себе і призведе до переповнення стека. Потрібне окреме сховище:this._nameабо#name. - Однакове ім'я для аксесора і поля.
get value()разом з полемvalue = 1не працює: поле екземпляра затінює аксесор із прототипу. - Очікувати аксесори в JSON. Обчислювані властивості не серіалізуються, бо неперелічувані і лежать на прототипі.
- Важкі обчислення в геттері. Читання властивості виглядає дешевим, тому виклик у циклі чи в рендері легко стає вузьким місцем; кешуйте результат.
- Побічні ефекти в геттері. Мережеві запити, мутації стану і логи роблять код непередбачуваним: геттер має лише повертати значення.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.