Пошук уроків, статей та іншого контенту
Реалізуєте наслідування класів через extends і super, а також розмежуєте екземплярні та статичні члени.
Наслідування дає змогу створити новий клас на основі вже наявного. Дочірній клас отримує доступ до методів і властивостей батьківського класу та може:
використовувати їх без дублювання коду;
додавати власну поведінку;
перевизначати успадковані методи;
розширювати логіку конструктора.
У JavaScript для цього використовують ключове слово extends.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getInfo() {
return `${this.name} — ${this.email}`;
}
}
class Admin extends User {
deleteUser(user) {
console.log(`Адміністратор ${this.name} видалив користувача ${user.name}`);
}
}
const admin = new Admin("Олена", "olena@example.com");
const user = new User("Андрій", "andrii@example.com");
console.log(admin.getInfo());
admin.deleteUser(user);Admin успадковує метод getInfo() від User, хоча цей метод не оголошений безпосередньо в Admin.
extendsКонструкція:
class Child extends Parent {
// тіло дочірнього класу
}створює зв’язок між прототипами:
екземпляр Child
↓
Child.prototype
↓
Parent.prototype
↓
Object.prototypeТому під час пошуку методу JavaScript спочатку перевіряє прототип дочірнього класу, а потім переходить до прототипу батьківського класу.
class Animal {
breathe() {
return "Дихання";
}
}
class Dog extends Animal {
bark() {
return "Гав-гав";
}
}
const dog = new Dog();
console.log(dog.bark());
console.log(dog.breathe());
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // trueОператор instanceof перевіряє весь ланцюжок прототипів. Тому екземпляр Dog одночасно є екземпляром і Dog, і Animal.
Якщо дочірній клас не має власного конструктора, JavaScript фактично використовує конструктор, який передає всі аргументи до батьківського:
class User {
constructor(name) {
this.name = name;
}
}
class Admin extends User {}
const admin = new Admin("Олена");
console.log(admin.name); // ОленаЯкщо дочірній клас має власний constructor, він повинен викликати super() до першого використання this.
class User {
constructor(name) {
this.name = name;
}
}
class Admin extends User {
constructor(name, permissions) {
super(name);
this.permissions = permissions;
}
}
const admin = new Admin("Олена", ["read", "delete"]);
console.log(admin.name);
console.log(admin.permissions);super()У конструкторі super() викликає конструктор батьківського класу:
super(name);У результаті батьківський клас ініціалізує свою частину об’єкта, зокрема властивість name.
У дочірньому класі не можна звертатися до this до виклику super():
class User {
constructor(name) {
this.name = name;
}
}
class Admin extends User {
constructor(name) {
// Помилка ReferenceError:
// this.role = "admin";
super(name);
this.role = "admin";
}
}Це обмеження стосується лише похідних класів — класів, оголошених через extends.
Дочірній клас може приймати додаткові аргументи й передавати батьківському лише потрібні значення:
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
class DiscountedProduct extends Product {
constructor(name, price, discount) {
super(name, price);
this.discount = discount;
}
getFinalPrice() {
return this.price * (1 - this.discount);
}
}
const product = new DiscountedProduct("Навушники", 2000, 0.15);
console.log(product.getFinalPrice()); // 1700Виклик super() не обов’язково має повторювати всі параметри дочірнього конструктора. Він повинен передати батьківському класу лише ті дані, які йому потрібні.
Дочірній клас може оголосити метод із таким самим іменем. У цьому разі він замінює успадковану реалізацію для екземплярів дочірнього класу.
class Notification {
send() {
console.log("Надсилання сповіщення");
}
}
class EmailNotification extends Notification {
send() {
console.log("Надсилання email");
}
}
const notification = new Notification();
const emailNotification = new EmailNotification();
notification.send();
emailNotification.send();Метод send() у EmailNotification повністю перевизначає метод батьківського класу.
superІноді потрібно не замінити, а розширити батьківську поведінку. Для цього використовують super.methodName():
class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
}
class TimestampLogger extends Logger {
log(message) {
const timestamp = new Date().toISOString();
super.log(`${timestamp}: ${message}`);
}
}
const logger = new TimestampLogger();
logger.log("Сервер запущено");super.log(...) викликає метод log() із прототипу батьківського класу. При цьому метод отримує правильний контекст this.
super у звичайних методахsuper можна використовувати не лише в конструкторі, а й у методах дочірнього класу:
class Account {
getDescription() {
return "Звичайний обліковий запис";
}
}
class PremiumAccount extends Account {
getDescription() {
return `${super.getDescription()} з преміум-доступом`;
}
}
const account = new PremiumAccount();
console.log(account.getDescription());
// Звичайний обліковий запис з преміум-доступомВиклик super.getDescription() не створює нового екземпляра. Він звертається до методу батьківського прототипу, а this залишається поточним екземпляром PremiumAccount.
Екземплярні члени належать конкретному об’єкту, створеному через new.
До них належать:
властивості, створені в конструкторі;
звичайні методи класу;
приватні екземплярні поля та методи.
class User {
constructor(name) {
this.name = name;
}
sayHello() {
return `Привіт, ${this.name}`;
}
}
const firstUser = new User("Олена");
const secondUser = new User("Андрій");
console.log(firstUser.name);
console.log(secondUser.name);
console.log(firstUser.sayHello());
console.log(secondUser.sayHello());Властивість name має кожен екземпляр окремо:
firstUser.name = "Марія";
console.log(firstUser.name); // Марія
console.log(secondUser.name); // АндрійЗвичайні методи класу зазвичай зберігаються в User.prototype, а не копіюються в кожен екземпляр. Це дає змогу спільно використовувати одну функцію між усіма об’єктами класу.
Статичні члени належать самому класу, а не його екземплярам.
Для оголошення статичного методу використовують static:
class MathHelper {
static double(value) {
return value * 2;
}
}
console.log(MathHelper.double(5)); // 10
// Помилка TypeError:
// const helper = new MathHelper();
// helper.double(5);Статичний метод викликають через ім’я класу:
ClassName.methodName();А не через екземпляр:
const instance = new ClassName();
instance.methodName(); // помилка, якщо метод статичнийclass User {
static role = "user";
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
static createGuest() {
return new User("Гість");
}
}
const user = User.createGuest();
console.log(User.role); // user
console.log(user.getName()); // Гість
// undefined, оскільки role належить User, а не user
console.log(user.role);Тут:
role — статична властивість класу;
createGuest() — статичний метод;
name — властивість екземпляра;
getName() — екземплярний метод.
Статичні властивості оголошують за допомогою static:
class Configuration {
static environment = "production";
static version = "2.4.0";
}
console.log(Configuration.environment);
console.log(Configuration.version);Зміна статичної властивості змінює значення, доступне через клас:
Configuration.environment = "development";
console.log(Configuration.environment); // developmentЕкземпляр не отримує статичні властивості через власний прототип:
const configuration = new Configuration();
console.log(configuration.environment); // undefinedСтатичний метод може бути фабрикою — методом, який створює екземпляри класу:
class User {
constructor(name, role) {
this.name = name;
this.role = role;
}
static fromObject(data) {
return new User(data.name, data.role);
}
describe() {
return `${this.name}: ${this.role}`;
}
}
const user = User.fromObject({
name: "Олена",
role: "admin"
});
console.log(user.describe());Такий підхід зручний, коли об’єкт потрібно створювати з різних форматів даних або за спеціальними правилами.
Статичні члени також можуть бути успадковані дочірнім класом.
class User {
static entityName = "User";
static describeEntity() {
return `Сутність: ${this.entityName}`;
}
}
class Admin extends User {
static entityName = "Admin";
}
console.log(Admin.entityName); // Admin
console.log(Admin.describeEntity()); // Сутність: AdminСтатичний ланцюжок наслідування працює окремо від ланцюжка екземплярів:
Admin
↓
User
↓
Function.prototypeА для екземпляра:
admin
↓
Admin.prototype
↓
User.prototype
↓
Object.prototypeТому Admin.describeEntity() знаходить метод через сам клас Admin, а не через Admin.prototype.
thisУ статичному методі this зазвичай посилається на клас, через який метод було викликано:
class Entity {
static getName() {
return this.name;
}
}
class User extends Entity {}
console.log(Entity.getName()); // Entity
console.log(User.getName()); // UserЦе дає змогу створювати статичні методи, які коректно працюють і для дочірніх класів.
superУ статичному методі super посилається на батьківський клас, а не на його прототип:
class BaseParser {
static parse(value) {
return String(value).trim();
}
}
class NumberParser extends BaseParser {
static parse(value) {
const normalizedValue = super.parse(value);
const result = Number(normalizedValue);
if (Number.isNaN(result)) {
throw new TypeError("Значення не є числом");
}
return result;
}
}
console.log(NumberParser.parse(" 42 ")); // 42У цьому прикладі:
super.parse(value);викликає статичний метод parse() батьківського класу BaseParser.
Наступний приклад поєднує:
наслідування;
super() у конструкторі;
перевизначення методу;
статичну властивість;
статичний фабричний метод;
статичний метод, успадкований дочірнім класом.
class Employee {
static company = "Tech Solutions";
constructor(name, salary) {
if (typeof name !== "string" || name.trim() === "") {
throw new TypeError("Ім'я має бути непорожнім рядком");
}
if (!Number.isFinite(salary) || salary < 0) {
throw new TypeError("Зарплата має бути невід'ємним числом");
}
this.name = name;
this.salary = salary;
}
getInfo() {
return `${this.name}, зарплата: ${this.salary}`;
}
static fromObject(data) {
return new this(data.name, data.salary);
}
}
class Developer extends Employee {
static company = "Tech Solutions";
constructor(name, salary, language) {
super(name, salary);
if (typeof language !== "string" || language.trim() === "") {
throw new TypeError("Мова програмування має бути непорожнім рядком");
}
this.language = language;
}
getInfo() {
return `${super.getInfo()}, мова: ${this.language}`;
}
writeCode() {
return `${this.name} пише код мовою ${this.language}`;
}
}
class Manager extends Employee {
constructor(name, salary, teamSize) {
super(name, salary);
if (!Number.isInteger(teamSize) || teamSize < 0) {
throw new TypeError("Розмір команди має бути невід'ємним цілим числом");
}
this.teamSize = teamSize;
}
getInfo() {
return `${super.getInfo()}, розмір команди: ${this.teamSize}`;
}
manageTeam() {
return `${this.name} керує командою з ${this.teamSize} людей`;
}
}
const developer = Developer.fromObject({
name: "Олена",
salary: 5000,
language: "JavaScript"
});
console.log(developer.getInfo());
console.log(developer.writeCode());
console.log(Developer.company);
const manager = new Manager("Андрій", 6500, 6);
console.log(manager.getInfo());
console.log(manager.manageTeam());
console.log(manager instanceof Employee); // trueУ цьому прикладі Employee.fromObject() використовує new this(...). Якщо метод викликати через Developer, значення this буде Developer, тому буде створено саме Developer, а не загальний Employee.
Приватні поля з префіксом # доступні лише всередині класу, де їх оголошено. Дочірній клас не може звернутися до приватного поля батьківського класу безпосередньо.
class Account {
#balance = 0;
deposit(amount) {
if (amount <= 0) {
throw new RangeError("Сума має бути більшою за нуль");
}
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
class PremiumAccount extends Account {
showBalance() {
// Помилка синтаксису:
// return this.#balance;
return this.getBalance();
}
}
const account = new PremiumAccount();
account.deposit(100);
console.log(account.showBalance()); // 100Дочірній клас може користуватися публічними методами батьківського класу, але не має прямого доступу до його приватних полів.
Якщо дочірньому класу потрібен доступ до даних, слід передбачити захищений через API класу спосіб:
публічний метод;
гетер;
статичний або екземплярний метод;
іншу абстракцію, яка не розкриває внутрішню реалізацію.
Методи класу, оголошені звичайним синтаксисом, розміщуються в прототипі:
class User {
sayHello() {
return "Привіт";
}
}Стрілкова функція, створена як властивість екземпляра, поводиться інакше:
class User {
sayHello = () => {
return `Привіт, ${this.name}`;
};
constructor(name) {
this.name = name;
}
}Така властивість створюється окремо для кожного екземпляра. Це може бути корисно для передавання методу як callback, оскільки стрілкова функція зберігає this. Водночас у великій кількості екземплярів вона споживає більше пам’яті, ніж спільний метод прототипу.
Наслідування варто використовувати, коли між типами справді є відношення «є»:
Developer є Employee;
Admin є User;
Circle є Shape.
Якщо один об’єкт лише використовує інший, краще застосувати композицію:
class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
}
class OrderService {
constructor(logger) {
this.logger = logger;
}
createOrder() {
this.logger.log("Замовлення створено");
}
}
const logger = new Logger();
const orderService = new OrderService(logger);
orderService.createOrder();OrderService не є Logger, тому наслідування тут було б неправильним. Він лише використовує логер як залежність.
this до super()class Admin extends User {
constructor(name) {
// Помилка
// this.name = name;
super(name);
}
}У похідному класі спочатку потрібно викликати super().
super() у конструкторіclass Admin extends User {
constructor(name) {
// Помилка: super() не викликано
this.role = "admin";
}
}Якщо дочірній клас має власний конструктор, він повинен викликати super() перед використанням this.
class User {
static createGuest() {
return new User("Гість");
}
}
const user = new User("Олена");
// Помилка:
// user.createGuest();Правильний виклик:
const guest = User.createGuest();super() і super.method()super() викликає конструктор батьківського класу;
super.method() викликає метод батьківського класу.
class Admin extends User {
constructor(name) {
super(name);
}
getInfo() {
return super.getInfo();
}
}Приватне поле з # не стає доступним у дочірньому класі. Потрібно використовувати публічний метод або гетер, передбачений батьківським класом.
Глибокі ієрархії класів ускладнюють програму:
Entity → User → Employee → Manager → SeniorManagerЗміна базового класу може вплинути на всі дочірні класи. За можливості краще віддавати перевагу невеликим класам і композиції.
this під час передавання методуЗвичайний метод може втратити контекст, якщо передати його окремо:
class Counter {
count = 0;
increment() {
this.count += 1;
}
}
const counter = new Counter();
const increment = counter.increment;
// Помилка або некоректний this:
// increment();Вирішенням може бути прив’язування методу:
const increment = counter.increment.bind(counter);
increment();
console.log(counter.count); // 1Або використання стрілкової властивості, якщо це виправдано дизайном класу.
extends створює дочірній клас на основі батьківського.
Дочірній клас успадковує методи через ланцюжок прототипів.
super() викликає конструктор батьківського класу.
super.method() викликає батьківський метод.
У похідному конструкторі не можна використовувати this до super().
Екземплярні члени належать конкретним об’єктам.
Статичні члени належать самому класу та викликаються через його ім’я.
Статичні методи й властивості можуть успадковуватися дочірніми класами.
Приватні поля # доступні лише в класі, де їх оголошено.
Наслідування доречне для відношення «є», а композиція — коли об’єкт лише використовує інший об’єкт.