Пошук уроків, статей та іншого контенту
Розберете синтаксис class як зручний запис прототипної моделі та створите класи з конструкторами й методами.
class у JavaScriptclass — це зручний синтаксис для створення об’єктів і роботи з прототипами. Класи з’явилися в ECMAScript 2015, але JavaScript і надалі залишається прототипною мовою.
Клас не створює окремий тип об’єктів у тому самому сенсі, що класи в деяких інших мовах. За допомогою class JavaScript створює функцію-конструктор і налаштовує її прототип.
Розглянемо простий клас:
class User {
constructor(name) {
this.name = name;
}
greet() {
return `Привіт, мене звати ${this.name}`;
}
}
const user = new User("Олена");
console.log(user.greet());
console.log(user instanceof User);Результат:
Привіт, мене звати Олена
trueУ цьому прикладі:
User — клас;
constructor — спеціальний метод, який викликається під час створення об’єкта;
name — властивість конкретного об’єкта;
greet — метод класу;
new User("Олена") створює новий об’єкт.
Конструктор описується спеціальним методом constructor:
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
const book = new Product("Книга", 450);
console.log(book.name);
console.log(book.price);Під час виконання new Product("Книга", 450) відбувається приблизно така послідовність:
Створюється новий порожній об’єкт.
Цей об’єкт пов’язується з Product.prototype.
Усередині конструктора this посилається на новий об’єкт.
Властивості name і price записуються в цей об’єкт.
Об’єкт повертається з конструктора.
Якщо клас не має власного конструктора, JavaScript використовує порожній конструктор автоматично:
class Empty {
// Конструктор створюється автоматично
}
const object = new Empty();Методи записують без ключового слова function:
class Counter {
constructor(initialValue = 0) {
this.value = initialValue;
}
increment() {
this.value += 1;
}
decrement() {
this.value -= 1;
}
getValue() {
return this.value;
}
}
const counter = new Counter(10);
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getValue()); // 11Методи класу доступні через прототип, а не копіюються в кожен створений об’єкт.
class User {
constructor(name) {
this.name = name;
}
greet() {
return `Привіт, ${this.name}`;
}
}
const firstUser = new User("Олена");
const secondUser = new User("Андрій");
console.log(firstUser.greet === secondUser.greet); // true
console.log(firstUser.hasOwnProperty("greet")); // false
console.log(User.prototype.hasOwnProperty("greet")); // trueМетод greet зберігається в User.prototype, тому всі екземпляри використовують одну функцію.
Це відрізняється від створення методу всередині конструктора:
function User(name) {
this.name = name;
this.greet = function () {
return `Привіт, ${this.name}`;
};
}У такому варіанті для кожного об’єкта створюється нова функція greet. Для великої кількості об’єктів це може бути менш ефективно.
class пов’язаний із прототипамиНаступні два варіанти мають подібну поведінку.
classclass User {
constructor(name) {
this.name = name;
}
greet() {
return `Привіт, ${this.name}`;
}
}
const user = new User("Олена");function User(name) {
this.name = name;
}
User.prototype.greet = function () {
return `Привіт, ${this.name}`;
};
const user = new User("Олена");У обох випадках:
створюється функція-конструктор;
об’єкти створюються за допомогою new;
метод greet знаходиться в прототипі;
об’єкт отримує доступ до методу через ланцюжок прототипів.
Клас робить код компактнішим і зрозумілішим, але не замінює прототипну модель JavaScript.
class User {
greet() {
return "Привіт";
}
}
const user = new User();
console.log(Object.getPrototypeOf(user) === User.prototype); // true
console.log(User.prototype.constructor === User); // true
console.log(typeof User); // functionКлас User має тип function, оскільки під капотом він представлений функцією-конструктором.
Властивості, які залежать від конкретного екземпляра, зазвичай створюють у конструкторі:
class Account {
constructor(owner, balance) {
this.owner = owner;
this.balance = balance;
}
deposit(amount) {
this.balance += amount;
}
}
const account = new Account("Марія", 1000);
console.log(account.owner); // Марія
console.log(account.balance); // 1000owner і balance належать самому об’єкту:
console.log(account.hasOwnProperty("owner")); // true
console.log(account.hasOwnProperty("balance")); // trueМетод deposit належить прототипу:
console.log(account.hasOwnProperty("deposit")); // false
console.log(Account.prototype.hasOwnProperty("deposit")); // trueТаке розділення є важливим:
дані конкретного об’єкта зберігаються в самому об’єкті;
спільна поведінка зберігається в прототипі.
newКласи не можна викликати як звичайні функції:
class User {
constructor(name) {
this.name = name;
}
}
// TypeError: Class constructor User cannot be invoked without 'new'
const user = User("Олена");Потрібно використовувати new:
const user = new User("Олена");Це одна з відмінностей класів від традиційних функцій-конструкторів. Функція-конструктор може бути викликана без new, хоча це часто призводить до помилок або небажаної поведінки.
this у методах класуУ звичайному виклику методу this посилається на об’єкт перед крапкою:
class User {
constructor(name) {
this.name = name;
}
greet() {
return `Привіт, ${this.name}`;
}
}
const user = new User("Олена");
console.log(user.greet()); // Привіт, ОленаАле якщо передати метод окремо, контекст може втратитися:
const greet = user.greet;
// TypeError або некоректне значення this
console.log(greet());Метод можна прив’язати до об’єкта за допомогою bind:
const boundGreet = user.greet.bind(user);
console.log(boundGreet()); // Привіт, ОленаЩе один варіант — створити стрілкову функцію-обгортку:
const greet = () => user.greet();
console.log(greet()); // Привіт, ОленаСтрілкові функції не мають власного this, тому вони використовують this із зовнішнього контексту.
Статичний метод належить самому класу, а не його екземплярам. Для його оголошення використовується static.
class MathHelper {
static double(value) {
return value * 2;
}
}
console.log(MathHelper.double(5)); // 10
// Екземпляр не має доступу до статичного методу
const helper = new MathHelper();
// helper.double(5); // TypeErrorСтатичні методи зручно використовувати для допоміжних операцій, які не залежать від конкретного об’єкта.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
static fromObject(data) {
return new User(data.name, data.email);
}
getDescription() {
return `${this.name}: ${this.email}`;
}
}
const user = User.fromObject({
name: "Олена",
email: "olena@example.com"
});
console.log(user.getDescription());Метод fromObject викликається через User.fromObject(), а не через user.fromObject().
Класи підтримують гетери й сетери. Вони дають змогу звертатися до методу як до властивості.
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
get area() {
return this.width * this.height;
}
set squareSize(size) {
this.width = size;
this.height = size;
}
}
const rectangle = new Rectangle(5, 10);
console.log(rectangle.area); // 50
rectangle.squareSize = 4;
console.log(rectangle.width); // 4
console.log(rectangle.height); // 4
console.log(rectangle.area); // 16Гетер має вигляд методу, але викликається без дужок:
console.log(rectangle.area);Сетер викликається під час присвоєння:
rectangle.squareSize = 4;Гетери корисні для обчислюваних значень, а сетери — для перевірки або перетворення даних перед записом.
Сучасний JavaScript підтримує приватні поля й методи з префіксом #.
class BankAccount {
#balance;
constructor(initialBalance = 0) {
if (initialBalance < 0) {
throw new Error("Початковий баланс не може бути від’ємним");
}
this.#balance = initialBalance;
}
deposit(amount) {
if (amount <= 0) {
throw new Error("Сума поповнення має бути додатною");
}
this.#balance += amount;
}
withdraw(amount) {
if (amount <= 0 || amount > this.#balance) {
throw new Error("Некоректна сума зняття");
}
this.#balance -= amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300До приватного поля не можна звернутися за межами класу:
// console.log(account.#balance); // SyntaxErrorПриватні поля допомагають приховати внутрішній стан об’єкта й змусити інший код користуватися публічними методами.
Назва приватного поля має бути оголошена в тілі класу до використання. Приватні поля не є властивостями, доступними через звичайний синтаксис object["#balance"].
Один клас може наслідувати інший за допомогою extends.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} видає звук`;
}
}
class Dog extends Animal {
speak() {
return `${this.name} гавкає`;
}
}
const dog = new Dog("Рекс");
console.log(dog.speak()); // Рекс гавкає
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // trueDog.prototype пов’язаний із Animal.prototype, тому об’єкти класу Dog можуть використовувати методи Animal.
Якщо дочірній клас не перевизначає метод, він успадковує його:
class Animal {
constructor(name) {
this.name = name;
}
move() {
return `${this.name} рухається`;
}
}
class Cat extends Animal {
meow() {
return `${this.name} нявкає`;
}
}
const cat = new Cat("Мурчик");
console.log(cat.move()); // Мурчик рухається
console.log(cat.meow()); // Мурчик нявкаєsuperКлючове слово super використовується для звернення до батьківського класу.
Якщо дочірній клас має власний constructor, він повинен викликати super() до використання this.
class User {
constructor(name) {
this.name = name;
}
}
class Admin extends User {
constructor(name, permissions) {
super(name);
this.permissions = permissions;
}
hasPermission(permission) {
return this.permissions.includes(permission);
}
}
const admin = new Admin("Олена", ["read", "write"]);
console.log(admin.name); // Олена
console.log(admin.hasPermission("write")); // trueБез super(name) спроба звернутися до this завершиться помилкою.
super також дає змогу викликати метод батьківського класу:
class Notification {
getMessage() {
return "Нове повідомлення";
}
}
class EmailNotification extends Notification {
getMessage() {
return `${super.getMessage()} на електронній пошті`;
}
}
const notification = new EmailNotification();
console.log(notification.getMessage());
// Нове повідомлення на електронній поштіНижче наведено приклад класу кошика покупок. Він демонструє конструктор, властивості екземпляра, методи, гетер і валідацію даних.
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(name, price, quantity = 1) {
if (typeof name !== "string" || name.trim() === "") {
throw new Error("Назва товару не може бути порожньою");
}
if (typeof price !== "number" || price < 0) {
throw new Error("Ціна має бути невід’ємним числом");
}
if (!Number.isInteger(quantity) || quantity <= 0) {
throw new Error("Кількість має бути додатним цілим числом");
}
this.items.push({
name,
price,
quantity
});
}
removeItem(name) {
this.items = this.items.filter((item) => item.name !== name);
}
get total() {
return this.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
}
getItemCount() {
return this.items.reduce(
(count, item) => count + item.quantity,
0
);
}
printSummary() {
if (this.items.length === 0) {
return "Кошик порожній";
}
const lines = this.items.map(
(item) => `${item.name}: ${item.quantity} × ${item.price} грн`
);
lines.push(`Разом: ${this.total} грн`);
return lines.join("\n");
}
}
const cart = new ShoppingCart();
cart.addItem("Клавіатура", 1800);
cart.addItem("Миша", 900, 2);
console.log(cart.getItemCount()); // 3
console.log(cart.total); // 3600
console.log(cart.printSummary());
cart.removeItem("Миша");
console.log(cart.total); // 1800Метод total оголошений як гетер, тому використовується як властивість:
console.log(cart.total);Значення обчислюється під час кожного звернення, тому воно залишається актуальним після додавання або видалення товарів.
Методи, оголошені в тілі класу, не є звичайними перелічуваними властивостями прототипу:
class User {
greet() {
return "Привіт";
}
}
console.log(Object.keys(User.prototype)); // []
console.log(Object.getOwnPropertyNames(User.prototype));
// ["constructor", "greet"]Object.keys повертає лише перелічувані властивості, а Object.getOwnPropertyNames — усі власні властивості, зокрема неперелічувані.
Це ще одна технічна відмінність між методами класу та методами, доданими звичайним присвоєнням:
function User() {}
User.prototype.greet = function () {
return "Привіт";
};
console.log(Object.keys(User.prototype)); // ["greet"]На практиці зазвичай достатньо пам’ятати, що методи класу зберігаються в прототипі та є спільними для всіх екземплярів.
Класи зручні, коли об’єкти:
мають спільну структуру;
містять дані та пов’язані з ними операції;
повинні створюватися багато разів;
мають чіткий життєвий цикл або правила зміни стану;
утворюють ієрархію через наслідування.
Наприклад, клас доречний для:
користувачів;
товарів;
банківських рахунків;
замовлень;
геометричних фігур;
елементів інтерфейсу;
клієнтів API або сервісів.
Клас не обов’язково потрібен для кожної структури даних. Якщо об’єкт містить лише кілька властивостей і не має складної поведінки, звичайний об’єкт часто буде простішим:
const user = {
name: "Олена",
email: "olena@example.com"
};newclass User {}
const user = User(); // TypeErrorПравильно:
const user = new User();this до super()class Parent {}
class Child extends Parent {
constructor() {
this.value = 10; // ReferenceError
super();
}
}Правильно:
class Child extends Parent {
constructor() {
super();
this.value = 10;
}
}class Timer {
constructor() {
this.seconds = 0;
}
increment() {
this.seconds += 1;
}
}
const timer = new Timer();
const increment = timer.increment;
// Контекст timer втрачено
// increment();Якщо метод передається як callback, прив’яжіть його:
const increment = timer.increment.bind(timer);
increment();
console.log(timer.seconds); // 1Масиви й об’єкти, які повинні бути окремими для кожного екземпляра, створюйте всередині конструктора:
class Team {
constructor() {
this.members = [];
}
}
const firstTeam = new Team();
const secondTeam = new Team();
firstTeam.members.push("Олена");
console.log(firstTeam.members); // ["Олена"]
console.log(secondTeam.members); // []Якщо випадково використати один зовнішній масив, команди можуть почати змінювати спільний стан.
_Префікс _ — це лише домовленість:
class User {
constructor(password) {
this._password = password;
}
}
const user = new User("secret");
console.log(user._password); // Доступ дозволенийДля справжнього приватного поля використовуйте #password:
class User {
#password;
constructor(password) {
this.#password = password;
}
checkPassword(value) {
return this.#password === value;
}
}class — зручний синтаксис для роботи з прототипною моделлю JavaScript.
Екземпляри класу створюються за допомогою new.
constructor налаштовує властивості нового об’єкта.
Методи класу зберігаються в ClassName.prototype і є спільними для екземплярів.
Статичні методи належать самому класу.
Гетери та сетери дають змогу працювати з методами як із властивостями.
extends створює наслідування між класами.
super() викликає конструктор батьківського класу, а super.method() — його метод.
Приватні поля з # приховують внутрішній стан класу.
Попри класовий синтаксис, основою об’єктної моделі JavaScript залишаються прототипи.