Перейти до основного вмісту

Класи

Синтаксис літерала об'єкта дозволяє створити один об'єкт. Проте, часто потрібно створити багато однотипних об'єктів з однаковим набором властивостей, але різними значеннями і методами для взаємодії з ними. Все це потрібно робити динамічно, під час виконання програми. З цією метою використовують класи - спеціальний синтаксис оголошення функції для створення об'єктів.

Оголошення класу

Оголошення класу починається з ключового слова class, після якого стоїть ім'я класу і фігурні дужки - його тіло. Класи прийнято називати з великої літери, а у назві відображати тип об'єкта (іменника), що створюється.

class User {
// Тіло класу
}

const mango = new User();
console.log(mango); // {}

const poly = new User();
console.log(poly); // {}

Результат виклику new User() - це об'єкт, який називається екземпляром класу, тому що містить дані і поведінку, що описуються класом.

Цікаво

Метод побудови класу залежить від того, що вам потрібно. У нашому випадку, клас - це користувач, тому ми додамо в нього поля для імені та пошти.

Конструктор класу

Для ініціалізації екземпляра в класі є метод constructor. Якщо він неоголошений, створюється конструктор за замовчуванням - порожня функція, яка не змінює екземпляр.

class User {
// Синтаксис оголошення методу класу
constructor(name, email) {
// Ініціалізація властивостей екземпляра
this.name = name;
this.email = email;
}
}

const mango = new User("Манго", "mango@mail.com");
console.log(mango); // { name: 'Манго', email: 'mango@mail.com' }

const poly = new User("Поли", "poly@mail.com");
console.log(poly); // { name: 'Поли', email: 'poly@mail.com' }

Виклик класу з оператором new призводить до створення нового об'єкта і виклику конструктора в контексті цього об'єкта. Тобто this всередині конструктора буде посилатися на новостворений об'єкт. Це дозволяє додавати кожному об'єкту властивості з однаковими іменами, але різними значеннями.

Властивості name та email називаються публічними властивостями, тому що вони будуть власними властивостями об'єкта-екземпляра і до них можна буде отримати доступ, звернувшись через крапку.

Об'єкт параметрів

Клас може приймати велику кількість вхідних даних для властивостей майбутнього об'єкта. Тому, до них також можна застосувати патерн «Об'єкт параметрів», передаючи один об'єкт з логічно іменованими властивостями, замість непов'язаного набору аргументів.

class User {
// Деструктуризуємо об'єкт
constructor({ name, email }) {
this.name = name;
this.email = email;
}
}

const mango = new User({
name: "Манго",
email: "mango@mail.com",
});
console.log(mango); // { name: "Манго", email: "mango@mail.com" }

const poly = new User({
name: "Поли",
email: "poly@mail.com",
});
console.log(poly); // { name: "Поли", email: "poly@mail.com" }

Методи класу

Для роботи з властивостями майбутнього екземпляра використовуються методи класу - функції, які будуть доступні екземпляру в його прототипі.

class User {
constructor({ name, email }) {
this.name = name;
this.email = email;
}

// Метод getEmail
getEmail() {
return this.email;
}

// Метод changeEmail
changeEmail(newEmail) {
this.email = newEmail;
}
}

Приватні властивості

Інкапсуляція - це концепція, що дозволяє приховати внутрішні деталі класу. Користувач класу повинен отримувати доступ тільки до публічного інтерфейсу - набору публічних властивостей і методів класу.

В класах інкапсуляція реалізується приватними властивостями, доступ до яких можна отримати тільки всередині класу.

Припустимо, що пошта користувача повинна бути недоступною для прямої зміни зовні, тобто - приватною. Додаючи до імені властивості символ #, ми робимо її приватною. Оголошення приватної властивості до ініціалізації в конструкторі - обов'язкове.

class User {
// Необов'язкове оголошення публічних властивостей
name;
// Обов'язкове оголошення приватних властивостей
#email;

constructor({ name, email }) {
this.name = name;
this.#email = email;
}

getEmail() {
return this.#email;
}

changeEmail(newEmail) {
this.#email = newEmail;
}
}

const mango = new User({
name: "Манго",
email: "mango@mail.com",
});
mango.changeEmail("mango@supermail.com");
console.log(mango.getEmail()); // mango@supermail.com
console.log(mango.#email); // Виникне помилка, це приватна властивість

Методи класу також можуть бути приватними, тобто доступні тільки у тілі класу. Для цього, перед їхнім ім'ям необхідно поставити символ #.

Геттери і сеттери

Геттери і сеттери - це коротший синтаксис оголошення методів для взаємодії з властивостями. Геттер і сеттер імітують звичайну публічну властивість класу, але дозволяють змінювати інші властивості зручнішим способом. Геттер виконується при спробі отримати значення властивості, а сеттер - при спробі його змінити.

Геттери і сеттери доречно використовувати для простих операцій читання і зміни значення властивостей, особливо приватних, як їх публічний інтерфейс. Для роботи з властивістю, яка зберігає масив або об'єкт, вони не підійдуть.

class User {
#email;

constructor({ name, email }) {
this.name = name;
this.#email = email;
}

// Геттер email
get email() {
return this.#email;
}

// Сеттер email
set email(newEmail) {
this.#email = newEmail;
}
}

Ми оголосили геттер і сеттер email, поставивши перед ім'ям властивості ключові слова get і set. Всередині цих методів ми або повертаємо значення приватної властивості #email, або змінюємо її значення. Геттер і сеттер застосовуються в парі і повинні називатися однаково.

const mango = new User({ name: "Манго", email: "mango@mail.com" });
console.log(mango.email); // mango@mail.com
mango.email = "mango@supermail.com";
console.log(mango.email); // mango@supermail.com

Звертаючись до mango.email, викликається геттер get email() {...} і виконується його код. При спробі запису mango.email = "mango@supermail.com" викликається сеттер set email(newEmail) {...} і рядок "mango@supermail.com" буде значенням параметра newEmail.

Перевага в тому, що це методи, а значить, під час запису можна виконати додатковий код, наприклад, з будь-якими перевірками, на відміну від виконання цієї ж операції безпосередньо з властивістю.

set email(newEmail) {
if(newEmail === "") {
console.error("Помилка! Пошта не може бути порожнім рядком!");
return;
}

this.#email = newEmail;
}

Статичні властивості

Крім публічних і приватних властивостей майбутнього екземпляра, в класі можна оголосити його власні властивості, доступні тільки класові, але не його екземплярам - статичні властивості (static). Вони корисні для зберігання інформації, що стосується класу.

Додамо класу користувача приватну властивість role - його роль, що визначає набір прав, наприклад, адміністратор, редактор, звичайний користувач тощо. Можливі ролі користувачів будемо зберігати як статичну властивість Roles - об'єкт з властивостями.

Статичні властивості оголошуються в тілі класу. Перед ім'ям властивості додається ключове слово static.

class User {
// Оголошення та ініціалізація статичної властивості
static Roles = {
ADMIN: "admin",
EDITOR: "editor",
};

#email;
#role;

constructor({ email, role }) {
this.#email = email;
this.#role = role;
}

get role() {
return this.#role;
}

set role(newRole) {
this.#role = newRole;
}
}

const mango = new User({
email: "mango@mail.com",
role: User.Roles.ADMIN,
});

console.log(mango.Roles); // undefined
console.log(User.Roles); // { ADMIN: "admin", EDITOR: "editor" }

console.log(mango.role); // "admin"
mango.role = User.Roles.EDITOR;
console.log(mango.role); // "editor"

Статичні властивості також можуть бути приватними, тобто доступними тільки всередині класу. Для цього ім'я властивості повинно починатися з символу #, так само, як приватні властивості. Звернення до приватної статичної властивості за межами тіла класу викличе помилку.

Статичні методи

У класі можна оголосити не тільки методи майбутнього екземпляра, а також методи, доступні тільки класу - статичні методи, які можуть бути як публічні, так і приватні. Синтаксис оголошення аналогічний статичним властивостям, за винятком того, що значенням буде метод.

class User {
static #takenEmails = [];

static isEmailTaken(email) {
return User.#takenEmails.includes(email);
}

#email;

constructor({ email }) {
this.#email = email;
User.#takenEmails.push(email);
}
}

const mango = new User({ email: "mango@mail.com" });

console.log(User.isEmailTaken("poly@mail.com"));
console.log(User.isEmailTaken("mango@mail.com"));

Особливість статичних методів у тому, що під час їх виклику ключове слово this посилається на сам клас. Це означає, що статичний метод може отримати доступ до статичних властивостей класу, але не до властивостей екземпляра. Логічно, тому що статичні методи викликає сам клас, а не його екземпляри.

Наслідування класів

Ключове слово extends дозволяє реалізувати наслідування класів, коли один клас (дочірній, похідний) наслідує властивості і методи іншого класу (батьківського).

class Child extends Parent {
// ...
}

У виразі class Child extends Parent дочірній клас Child наслідує (розширює) від батьківського класу Parent.

Це означає, що ми можемо оголосити базовий клас, який зберігає загальні характеристики і методи для групи похідних класів, які наслідують властивості і методи батьківського, але також додають свої унікальні.

Наприклад, у застосунку є користувачі з різними ролями - адміністратор, копірайтер, контент менеджер тощо. У кожного типу користувача є набір загальних характеристик, наприклад, пошта і пароль, але також є й унікальні.

Створивши незалежні класи для кожного типу користувача, ми отримаємо дублювання загальних властивостей і методів, і, якщо необхідно змінити, наприклад, назву властивості, доведеться проходити по усіх класах, а це незручно і вимагає багато часу.

Замість цього, можна створити загальний клас User, який буде зберігати набір загальних властивостей і методів, після чого, створити класи для кожного типу користувача, які наслідують цей набір від класу User. За потреби змінити щось спільне, достатньо буде змінити тільки код класу User.

class User {
#email;

constructor(email) {
this.#email = email;
}

get email() {
return this.#email;
}

set email(newEmail) {
this.#email = newEmail;
}
}

class ContentEditor extends User {
// Тіло класу ContentEditor
}

const editor = new ContentEditor("mango@mail.com");
console.log(editor); // { email: "mango@mail.com" }
console.log(editor.email); // "mango@mail.com"

Клас ContentEditor наслідує від класу User його конструктор, геттер і сеттер email, а також однойменну публічну властивість. Важливо пам'ятати, що приватні властивості і методи батьківського класу не наслідуються дочірнім класом.

Конструктор дочірнього класу

Насамперед в конструкторі дочірнього класу необхідно викликати спеціальну функцію super(аргументи) - це псевдонім конструктора батьківського класу. В іншому випадку, при спробі звернутися до this в конструкторі дочірнього класу, виникне помилка. Під час виклику конструктора батьківського класу передаємо необхідні йому аргументи для ініціалізації властивостей.

class User {
#email;

constructor(email) {
this.#email = email;
}

get email() {
return this.#email;
}

set email(newEmail) {
this.#email = newEmail;
}
}

class ContentEditor extends User {
constructor({ email, posts }) {
// Виклик конструктора батьківського класу User
super(email);
this.posts = posts;
}
}

const editor = new ContentEditor({ email: "mango@mail.com", posts: [] });
console.log(editor); // { email: 'mango@mail.com', posts: [] }
console.log(editor.email); // 'mango@mail.com'

Методи дочірнього класу

В дочірньому класі можна оголошувати методи, які будуть доступні тільки його екземплярам.

// Уявімо, що вище є оголошення класу User

class ContentEditor extends User {
constructor({ email, posts }) {
super(email);
this.posts = posts;
}

addPost(post) {
this.posts.push(post);
}
}

const editor = new ContentEditor({ email: "mango@mail.com", posts: [] });
console.log(editor); // { email: 'mango@mail.com', posts: [] }
console.log(editor.email); // 'mango@mail.com'
editor.addPost("post-1");
console.log(editor.posts); // ['post-1']