Пошук уроків, статей та іншого контенту
Перевизначатимете методи та використовуватимете поліморфізм для єдиного інтерфейсу різних типів об’єктів.
Перевизначення методу — це створення в дочірньому класі методу з таким самим ім’ям, як у батьківському класі, але з іншою реалізацією.
Дочірній клас успадковує метод, якщо не оголошує власну версію. Якщо ж метод оголошено в дочірньому класі, JavaScript використовує саме його.
class User {
constructor(name) {
this.name = name;
}
getRole() {
return "Користувач";
}
describe() {
return `${this.name}: ${this.getRole()}`;
}
}
class Admin extends User {
getRole() {
return "Адміністратор";
}
}
class Moderator extends User {
getRole() {
return "Модератор";
}
}
const user = new User("Олена");
const admin = new Admin("Андрій");
const moderator = new Moderator("Марія");
console.log(user.describe()); // Олена: Користувач
console.log(admin.describe()); // Андрій: Адміністратор
console.log(moderator.describe()); // Марія: МодераторМетод describe() оголошено лише в класі User, але всередині він викликає this.getRole().
Під час виконання:
для об’єкта User викликається User.prototype.getRole;
для об’єкта Admin — Admin.prototype.getRole;
для об’єкта Moderator — Moderator.prototype.getRole.
Це означає, що батьківський метод може працювати з різними реалізаціями дочірніх класів.
superІноді дочірній клас має не повністю замінити метод, а розширити його поведінку. Для цього використовується super.
У методі дочірнього класу вираз super.methodName() викликає відповідний метод батьківського класу.
class Employee {
constructor(name) {
this.name = name;
}
introduce() {
return `Мене звати ${this.name}.`;
}
}
class Developer extends Employee {
constructor(name, language) {
super(name);
this.language = language;
}
introduce() {
const parentMessage = super.introduce();
return `${parentMessage} Я розробник і пишу мовою ${this.language}.`;
}
}
const developer = new Developer("Ірина", "JavaScript");
console.log(developer.introduce());
// Мене звати Ірина. Я розробник і пишу мовою JavaScript.У цьому прикладі:
super(name) викликає конструктор Employee;
super.introduce() отримує базове повідомлення;
дочірній клас додає власну інформацію.
Якщо дочірній клас має конструктор, він повинен викликати super() до використання this.
Поліморфізм означає, що різні типи об’єктів можуть використовувати спільний інтерфейс, але реалізовувати його по-різному.
У JavaScript немає обов’язкових інтерфейсів, як у деяких статично типізованих мовах. Проте об’єкти можуть дотримуватися домовленості: наприклад, кожен об’єкт оплати повинен мати метод pay().
Код, який працює з такими об’єктами, не мусить знати їхній конкретний клас.
class CardPayment {
constructor(cardNumber) {
this.cardNumber = cardNumber;
}
pay(amount) {
const lastFourDigits = this.cardNumber.slice(-4);
return `Оплачено ${amount} грн карткою ****${lastFourDigits}`;
}
}
class CashPayment {
pay(amount) {
return `Оплачено ${amount} грн готівкою`;
}
}
class PayPalPayment {
constructor(email) {
this.email = email;
}
pay(amount) {
return `Оплачено ${amount} грн через PayPal (${this.email})`;
}
}
function processPayment(paymentMethod, amount) {
console.log(paymentMethod.pay(amount));
}
const payments = [
new CardPayment("4111111111111111"),
new CashPayment(),
new PayPalPayment("customer@example.com")
];
for (const payment of payments) {
processPayment(payment, 1000);
}Результат:
Оплачено 1000 грн карткою ****1111
Оплачено 1000 грн готівкою
Оплачено 1000 грн через PayPal (customer@example.com)Функція processPayment() працює з параметром paymentMethod, не перевіряючи його клас:
function processPayment(paymentMethod, amount) {
console.log(paymentMethod.pay(amount));
}Їй важливо лише, щоб переданий об’єкт мав метод pay().
Це приклад поліморфізму через спільний інтерфейс.
Поліморфізм часто використовують разом із наслідуванням. Батьківський клас визначає спільний метод, а дочірні класи перевизначають його.
Розглянемо систему сповіщень:
class Notification {
constructor(message) {
this.message = message;
}
send() {
throw new Error("Метод send() має бути перевизначений");
}
}
class EmailNotification extends Notification {
constructor(email, message) {
super(message);
this.email = email;
}
send() {
return `Email надіслано на ${this.email}: ${this.message}`;
}
}
class SmsNotification extends Notification {
constructor(phone, message) {
super(message);
this.phone = phone;
}
send() {
return `SMS надіслано на ${this.phone}: ${this.message}`;
}
}
class PushNotification extends Notification {
constructor(deviceId, message) {
super(message);
this.deviceId = deviceId;
}
send() {
return `Push-сповіщення надіслано на пристрій ${this.deviceId}: ${this.message}`;
}
}
function sendNotifications(notifications) {
for (const notification of notifications) {
console.log(notification.send());
}
}
const notifications = [
new EmailNotification(
"user@example.com",
"Ваше замовлення прийнято"
),
new SmsNotification(
"+380501234567",
"Кур’єр уже прямує до вас"
),
new PushNotification(
"device-123",
"Замовлення доставлено"
)
];
sendNotifications(notifications);Функція sendNotifications() має єдиний алгоритм для всіх сповіщень:
notification.send();Проте кожен тип об’єкта виконує send() по-своєму.
Батьківський метод у цьому випадку використовується як контракт. Він повідомляє, що кожен дочірній клас повинен надати власну реалізацію send().
У JavaScript метод батьківського класу можна зробити таким, що повідомляє про помилку, якщо його не перевизначено:
class Shape {
getArea() {
throw new Error("Метод getArea() не реалізовано");
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
const rectangle = new Rectangle(4, 5);
console.log(rectangle.getArea()); // 20Якщо створити клас-нащадок без реалізації getArea() і викликати цей метод, буде викинуто помилку:
class UnknownShape extends Shape {}
const shape = new UnknownShape();
shape.getArea();
// Error: Метод getArea() не реалізованоТакий підхід допомагає виявляти помилки раніше. Він особливо корисний, коли батьківський клас призначений для розширення, а не для створення безпосередніх об’єктів.
Для поліморфізму в JavaScript не обов’язково використовувати класи. Достатньо, щоб об’єкти мали однакову назву методу.
const square = {
getArea() {
return 5 * 5;
}
};
const circle = {
getArea() {
return Math.PI * 3 ** 2;
}
};
function printArea(shape) {
console.log(`Площа: ${shape.getArea()}`);
}
printArea(square);
printArea(circle);Функція printArea() працює з будь-яким об’єктом, який має метод getArea().
Цей стиль відповідає ідеї структурної типізації: важливо не те, до якого класу належить об’єкт, а те, які операції він підтримує.
Коли викликається метод об’єкта, JavaScript шукає його:
безпосередньо в об’єкті;
у прототипі об’єкта;
у прототипі батьківського класу;
далі по ланцюжку прототипів.
class Animal {
speak() {
return "Тварина видає звук";
}
}
class Dog extends Animal {
speak() {
return "Гав-гав";
}
}
const dog = new Dog();
console.log(dog.speak()); // Гав-гавУ Dog є власна версія speak(), тому вона перекриває метод Animal.
Якщо видалити метод speak() із Dog, виклик перейде до Animal:
class Animal {
speak() {
return "Тварина видає звук";
}
}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog.speak()); // Тварина видає звукВажливо розрізняти:
перевизначення — дочірній клас має метод із тим самим ім’ям;
перевантаження — кілька методів з одним ім’ям, але різними параметрами.
Класичного перевантаження методів у JavaScript немає. Якщо оголосити кілька методів з однаковим ім’ям у класі, останнє оголошення замінить попередні.
Поліморфізм доречний, коли:
різні об’єкти виконують одну загальну операцію;
потрібно додавати нові типи без зміни основного алгоритму;
код має працювати з абстракцією, а не з конкретним класом;
умовних конструкцій if...else або switch стає забагато.
Наприклад, замість такого коду:
function sendNotification(type, data) {
if (type === "email") {
// Логіка email
} else if (type === "sms") {
// Логіка SMS
} else if (type === "push") {
// Логіка push
}
}можна створити окремі об’єкти зі спільним методом send():
function sendNotification(notification) {
notification.send();
}Тоді додавання нового каналу сповіщень не потребує зміни функції sendNotification().
Поліморфний код зазвичай не перевіряє конкретний тип об’єкта:
function calculateArea(shape) {
return shape.getArea();
}Натомість код із перевірками типів може виглядати так:
function calculateArea(shape) {
if (shape instanceof Rectangle) {
return shape.width * shape.height;
}
if (shape instanceof Circle) {
return Math.PI * shape.radius ** 2;
}
throw new Error("Невідома фігура");
}Другий варіант сильніше пов’язує функцію з конкретними класами. Якщо додати Triangle, доведеться змінити calculateArea().
У поліморфному варіанті новий клас сам реалізує getArea(), а функція залишається без змін:
class Triangle {
constructor(base, height) {
this.base = base;
this.height = height;
}
getArea() {
return (this.base * this.height) / 2;
}
}Це сприяє розширюваності коду.
super() у конструкторіЯкщо дочірній клас має конструктор, потрібно викликати super() до звернення до this.
class User {
constructor(name) {
this.name = name;
}
}
class Admin extends User {
constructor(name, permissions) {
super(name);
this.permissions = permissions;
}
}Без super(name) JavaScript викине помилку під час створення об’єкта.
Поліморфізм працює лише тоді, коли всі об’єкти дотримуються спільного інтерфейсу.
function processPayment(payment) {
return payment.pay(100);
}
processPayment({});
// TypeError: payment.pay is not a functionПотрібно або контролювати об’єкти, які передаються у функцію, або перевіряти наявність методу:
function processPayment(payment) {
if (typeof payment.pay !== "function") {
throw new TypeError("Об’єкт не підтримує метод pay()");
}
return payment.pay(100);
}supersuper посилається на батьківський клас, а не на поточний об’єкт як звичайна властивість.
Правильно:
class Child extends Parent {
method() {
return super.method();
}
}Виклик super.method() має сенс лише в контексті класу, який успадковує Parent.
thisЯкщо метод передати як окрему функцію, він може втратити контекст об’єкта:
class Counter {
constructor() {
this.value = 0;
}
increment() {
this.value += 1;
}
}
const counter = new Counter();
const increment = counter.increment;
// increment(); // this буде undefined у strict modeДля збереження контексту можна використати стрілкову функцію або прив’язати метод:
const boundIncrement = counter.increment.bind(counter);
boundIncrement();
console.log(counter.value); // 1Не кожну спільну поведінку потрібно реалізовувати через ієрархію класів. Якщо об’єктам достатньо мати однаковий метод, можна використати звичайні об’єкти або композицію.
Наслідування доречне, коли між типами справді є відношення «є різновидом», а не лише спільна назва операції.
Створіть систему доставки:
Базовий клас Delivery із методом calculateCost(distance).
Клас CourierDelivery, у якому вартість становить 50 + distance * 10.
Клас PostDelivery, у якому вартість становить 30 + distance * 5.
Клас PickupDelivery, у якому доставка завжди безкоштовна.
Функцію printDeliveryCost(delivery, distance), яка працює з усіма типами через метод calculateCost().
Очікуваний принцип використання:
const deliveries = [
new CourierDelivery(),
new PostDelivery(),
new PickupDelivery()
];
for (const delivery of deliveries) {
printDeliveryCost(delivery, 10);
}Функція не повинна перевіряти клас об’єкта через instanceof. Вона має використовувати спільний метод calculateCost().
Перевизначення методу — це створення в дочірньому класі власної реалізації успадкованого методу.
super.method() викликає реалізацію методу з батьківського класу.
super() у конструкторі викликає конструктор батьківського класу.
Поліморфізм дає змогу працювати з різними об’єктами через спільний інтерфейс.
Для поліморфізму важлива підтримка потрібного методу, а не конкретний клас об’єкта.
Поліморфний код зменшує кількість перевірок типів і спрощує додавання нових реалізацій.
Батьківський метод можна використовувати як контракт, викидаючи помилку, якщо дочірній клас його не реалізував.