Пошук уроків, статей та іншого контенту
Керуватимете доступом до властивостей через гетери й сетери та додаватимете перевірку значень.
Звичайна властивість об’єкта дає змогу напряму читати й змінювати значення:
const user = {
age: 25
};
user.age = -10;У цьому прикладі об’єкт приймає некоректне значення. Гетери та сетери дають змогу перехопити операції читання й запису:
гетер виконується під час читання властивості;
сетер виконується під час присвоєння значення властивості;
у сетері можна перевіряти, перетворювати або відхиляти дані;
зовнішній код працює зі звичайним синтаксисом object.property.
Гетер оголошується за допомогою ключового слова get:
const user = {
firstName: "Олена",
lastName: "Шевченко",
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
};
console.log(user.fullName); // Олена ШевченкоfullName виглядає як властивість, але фактично під час кожного читання виконується функція-гетер.
Викликати гетер як функцію не потрібно:
console.log(user.fullName); // правильно
// console.log(user.fullName()); // помилкаГетери зручно використовувати для:
обчислюваних властивостей;
форматування даних;
надання доступу до внутрішнього значення в потрібному форматі;
приховування деталей реалізації.
Сетер оголошується за допомогою ключового слова set і приймає рівно один параметр:
const product = {
_price: 0,
get price() {
return this._price;
},
set price(value) {
if (typeof value !== "number" || value < 0) {
throw new Error("Ціна має бути невід’ємним числом");
}
this._price = value;
}
};
product.price = 150;
console.log(product.price); // 150
product.price = -20; // Error: Ціна має бути невід’ємним числомСетер виконується під час присвоєння:
product.price = 200;Цей запис фактично викликає сетер і передає йому значення 200.
Гетер або сетер не повинні звертатися до тієї самої властивості, для якої вони оголошені. Інакше виникне нескінченна рекурсія:
const account = {
get balance() {
return this.balance; // помилка: гетер викликає сам себе
},
set balance(value) {
this.balance = value; // помилка: сетер викликає сам себе
}
};Зазвичай використовують окрему внутрішню властивість, наприклад _balance:
const account = {
_balance: 0,
get balance() {
return this._balance;
},
set balance(value) {
if (value < 0) {
throw new Error("Баланс не може бути від’ємним");
}
this._balance = value;
}
};Префікс _ — це лише домовленість. Він не робить властивість справді приватною. Код із зовнішнього боку все ще може виконати:
account._balance = -1000;Для справжнього приховування даних у сучасному JavaScript використовують приватні поля класу.
Гетери й сетери можна оголошувати всередині класу:
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
get area() {
return this.width * this.height;
}
set width(value) {
if (typeof value !== "number" || value <= 0) {
throw new Error("Ширина має бути додатним числом");
}
this._width = value;
}
get width() {
return this._width;
}
set height(value) {
if (typeof value !== "number" || value <= 0) {
throw new Error("Висота має бути додатним числом");
}
this._height = value;
}
get height() {
return this._height;
}
}
const rectangle = new Rectangle(10, 5);
console.log(rectangle.width); // 10
console.log(rectangle.height); // 5
console.log(rectangle.area); // 50
rectangle.width = 20;
console.log(rectangle.area); // 100
// rectangle.height = 0; // помилка валідаціїУ конструкторі:
this.width = width;
this.height = height;викликаються сетери width і height. Тому початкові значення також проходять перевірку.
Поля з префіксом # є приватними. Доступ до них можливий лише всередині того самого класу:
class BankAccount {
#balance;
constructor(initialBalance = 0) {
this.balance = initialBalance;
}
get balance() {
return this.#balance;
}
set balance(value) {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new TypeError("Баланс має бути скінченним числом");
}
if (value < 0) {
throw new RangeError("Баланс не може бути від’ємним");
}
this.#balance = value;
}
deposit(amount) {
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) {
throw new RangeError("Сума поповнення має бути додатним числом");
}
this.#balance += amount;
}
withdraw(amount) {
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) {
throw new RangeError("Сума зняття має бути додатним числом");
}
if (amount > this.#balance) {
throw new Error("Недостатньо коштів");
}
this.#balance -= amount;
}
}
const account = new BankAccount(100);
console.log(account.balance); // 100
account.deposit(50);
console.log(account.balance); // 150
account.withdraw(30);
console.log(account.balance); // 120
// account.#balance = 1000000;
// SyntaxError: приватне поле недоступне ззовніУ цьому прикладі:
#balance неможливо змінити безпосередньо ззовні;
сетер balance контролює присвоєння;
методи deposit і withdraw змінюють баланс за правилами;
гетер balance надає безпечний доступ для читання.
Це приклад інкапсуляції: внутрішній стан об’єкта захищений, а взаємодія з ним відбувається через визначений інтерфейс.
Гетер і сетер не обов’язково оголошувати разом.
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
const user = new User("Ірина", "Коваленко");
console.log(user.fullName); // Ірина Коваленко
// user.fullName = "Інше ім’я";Якщо сетер не оголошено, властивість не має звичайного способу зміни через присвоєння. У строгому режимі спроба присвоєння призведе до помилки.
Сетер можна використати, наприклад, щоб приймати пароль і зберігати не сам пароль, а його оброблене представлення:
class UserCredentials {
set password(value) {
if (typeof value !== "string" || value.length < 8) {
throw new Error("Пароль має містити щонайменше 8 символів");
}
this._passwordLength = value.length;
}
get passwordLength() {
return this._passwordLength;
}
}
const credentials = new UserCredentials();
credentials.password = "secure123";
console.log(credentials.passwordLength); // 9У реальному застосунку пароль не слід зберігати у відкритому вигляді. Його потрібно обробляти спеціалізованими безпечними засобами на сервері.
Сетер може не лише відхиляти некоректні значення, а й нормалізувати їх:
class Contact {
#email = "";
get email() {
return this.#email;
}
set email(value) {
if (typeof value !== "string") {
throw new TypeError("Email має бути рядком");
}
const normalizedEmail = value.trim().toLowerCase();
if (!normalizedEmail.includes("@")) {
throw new Error("Некоректний email");
}
this.#email = normalizedEmail;
}
}
const contact = new Contact();
contact.email = " USER@EXAMPLE.COM ";
console.log(contact.email); // user@example.comУ сетері відбуваються три операції:
перевірка типу;
очищення та нормалізація значення;
перевірка формату.
thisГетери та сетери працюють із конкретним екземпляром через this:
class Temperature {
#celsius = 0;
get celsius() {
return this.#celsius;
}
set celsius(value) {
this.#celsius = value;
}
get fahrenheit() {
return this.#celsius * 9 / 5 + 32;
}
}
const temperature = new Temperature();
temperature.celsius = 20;
console.log(temperature.celsius); // 20
console.log(temperature.fahrenheit); // 68Тут fahrenheit не зберігається окремо. Він обчислюється на основі #celsius, тому значення не може застаріти після зміни температури.
Користувач класу не повинен знати, як саме зберігається значення:
class User {
#firstName;
#lastName;
constructor(firstName, lastName) {
this.#firstName = firstName;
this.#lastName = lastName;
}
get displayName() {
return `${this.#lastName}, ${this.#firstName}`;
}
set displayName(value) {
const parts = value.split(" ");
if (parts.length !== 2) {
throw new Error("Ім’я має складатися з двох слів");
}
this.#firstName = parts[0];
this.#lastName = parts[1];
}
}
const user = new User("Олена", "Бондар");
console.log(user.displayName); // Бондар, Олена
user.displayName = "Марія Петренко";
console.log(user.displayName); // Петренко, МаріяЗовнішній код використовує displayName, а клас сам вирішує, як розділити значення та де його зберегти.
Гетери та сетери є частиною дескриптора властивості. Подивитися дескриптор можна за допомогою Object.getOwnPropertyDescriptor:
const user = {
firstName: "Олена",
get greeting() {
return `Привіт, ${this.firstName}!`;
}
};
const descriptor = Object.getOwnPropertyDescriptor(user, "greeting");
console.log(descriptor);
// {
// get: [Function: get greeting],
// set: undefined,
// enumerable: true,
// configurable: true
// }Для accessor-властивості дескриптор може містити:
get — функцію-гетер;
set — функцію-сетер;
enumerable — чи потрапляє властивість у перелік властивостей;
configurable — чи можна змінювати дескриптор.
Це корисно під час роботи з метапрограмуванням, бібліотеками та низькорівневим налаштуванням об’єктів. У більшості прикладних задач достатньо синтаксису get і set у класі або літералі об’єкта.
class User {
get name() {
return "Олена";
}
}
const user = new User();
console.log(user.name); // правильно
// console.log(user.name()); // помилкаГетер викликається під час доступу до властивості, тому круглі дужки не потрібні.
class Product {
set price(value) {
this.price = value; // помилка: сетер викликає сам себе
}
}Потрібно використовувати інше сховище:
class Product {
set price(value) {
this._price = value;
}
get price() {
return this._price;
}
}Або приватне поле:
class Product {
#price;
set price(value) {
this.#price = value;
}
get price() {
return this.#price;
}
}class Product {
set price(value) {
if (value < 0) {
throw new Error("Ціна не може бути від’ємною");
}
this._price = value;
}
}Такий сетер пропустить, наприклад, рядок "100" або null. Перевіряйте не лише діапазон, а й тип та спеціальні випадки:
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
throw new Error("Ціна має бути скінченним невід’ємним числом");
}Не кожна властивість потребує accessor-логіки. Якщо значення не треба перевіряти, перетворювати або обчислювати, звичайна властивість може бути простішою та зрозумілішою.
Гетер зазвичай має лише повертати значення. Не варто змінювати стан об’єкта, виконувати непередбачувані дії або робити важкі операції під час простого читання властивості:
class Report {
get data() {
// Погано: читання властивості несподівано змінює стан
this.loadCount++;
return this._data;
}
}Гетер, який викликає мережевий запит або іншу асинхронну операцію, також є невдалим дизайном. Для таких дій краще використовувати явний метод, наприклад loadData().
Створіть клас Order, який має:
приватне поле #status;
гетер status;
сетер status, що дозволяє лише значення "new", "paid" або "shipped";
метод pay(), який змінює статус на "paid";
метод ship(), який змінює статус на "shipped" лише після оплати.
Приклад очікуваної поведінки:
const order = new Order();
console.log(order.status); // new
order.pay();
console.log(order.status); // paid
order.ship();
console.log(order.status); // shippedСпроба відправити неоплачене замовлення або встановити невідомий статус має завершуватися помилкою.
Гетер читає значення як властивість, але виконує функцію.
Сетер перехоплює присвоєння та може перевіряти вхідні дані.
Гетери й сетери використовуються для обчислюваних властивостей, нормалізації та контролю доступу.
Для зберігання значення не можна звертатися в гетері або сетері до тієї самої властивості.
Префікс _ є лише домовленістю, а приватні поля #name забезпечують справжню приватність.
Сетер має перевіряти тип, діапазон і формат значення.
Гетери бажано робити передбачуваними та без побічних ефектів.
Інкапсуляція дає змогу змінювати внутрішню реалізацію класу, не змінюючи його публічний інтерфейс.