Пошук уроків, статей та іншого контенту
Створите дочірній клас, успадкуєте поведінку батьківського та розширите її власними властивостями й методами.
Успадкування дає змогу створити новий клас на основі вже наявного. Новий клас називають дочірнім, а клас, від якого він успадковує поведінку, — батьківським.
Дочірній клас отримує:
методи батьківського класу;
доступ до його публічних властивостей;
можливість перевизначати методи;
можливість додавати власні властивості та методи.
У JavaScript для успадкування використовується ключове слово extends.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} видає звук`;
}
}
class Dog extends Animal {
fetch() {
return `${this.name} приніс м'яч`;
}
}
const dog = new Dog("Бадді");
console.log(dog.name); // Бадді
console.log(dog.speak()); // Бадді видає звук
console.log(dog.fetch()); // Бадді приніс м'ячКлас Dog успадковує метод speak() і властивість name від класу Animal, а також додає власний метод fetch().
extendsЗагальна форма успадкування:
class Child extends Parent {
// властивості та методи дочірнього класу
}Наприклад:
class User {
login() {
return "Користувач увійшов у систему";
}
}
class Admin extends User {
deleteUser() {
return "Користувача видалено";
}
}
const admin = new Admin();
console.log(admin.login());
console.log(admin.deleteUser());Об’єкт admin може викликати як власний метод deleteUser(), так і успадкований метод login().
Дочірній клас може мати власний constructor. Щоб викликати конструктор батьківського класу, використовується super().
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", "delete"]);
console.log(admin.name); // Олена
console.log(admin.hasPermission("delete")); // trueВиклик super(name) передає значення до конструктора класу User. У результаті батьківський конструктор створює властивість name.
У дочірньому конструкторі super() потрібно викликати до звернення до this:
class Child extends Parent {
constructor() {
super();
this.value = 10;
}
}Якщо дочірній клас не має власного конструктора, JavaScript автоматично створює конструктор, який передає аргументи батьківському класу:
class Parent {
constructor(value) {
this.value = value;
}
}
class Child extends Parent {}
const object = new Child(42);
console.log(object.value); // 42Ключове слово super використовується не лише в конструкторі. Через super можна викликати метод батьківського класу.
class Notification {
send(message) {
return `Повідомлення: ${message}`;
}
}
class EmailNotification extends Notification {
send(message) {
const result = super.send(message);
return `${result} Надіслано електронною поштою.`;
}
}
const notification = new EmailNotification();
console.log(notification.send("Ваше замовлення готове"));
// Повідомлення: Ваше замовлення готове Надіслано електронною поштою.У цьому прикладі метод send() дочірнього класу:
викликає реалізацію з батьківського класу;
отримує її результат;
доповнює цей результат власною поведінкою.
Дочірній клас може мати метод із таким самим іменем, як у батьківського. Це називається перевизначенням методу.
class Payment {
pay(amount) {
return `Оплата на суму ${amount} грн`;
}
}
class CardPayment extends Payment {
pay(amount) {
return `Оплата карткою: ${amount} грн`;
}
}
class CashPayment extends Payment {
pay(amount) {
return `Оплата готівкою: ${amount} грн`;
}
}
const payments = [
new CardPayment(),
new CashPayment()
];
for (const payment of payments) {
console.log(payment.pay(500));
}Кожен дочірній клас має власну реалізацію pay(), хоча обидва класи походять від Payment.
Розглянемо приклад із працівниками компанії.
Базовий клас Employee містить спільні дані та поведінку всіх працівників. Клас Developer розширює його властивістю language, а клас Manager — властивістю teamSize.
class Employee {
constructor(name, salary) {
this.name = name;
this.salary = salary;
}
describe() {
return `${this.name} отримує ${this.salary} грн`;
}
work() {
return `${this.name} виконує роботу`;
}
}
class Developer extends Employee {
constructor(name, salary, language) {
super(name, salary);
this.language = language;
}
work() {
return `${this.name} пише код мовою ${this.language}`;
}
reviewCode() {
return `${this.name} перевіряє код`;
}
}
class Manager extends Employee {
constructor(name, salary, teamSize) {
super(name, salary);
this.teamSize = teamSize;
}
work() {
return `${this.name} керує командою з ${this.teamSize} людей`;
}
organizeMeeting() {
return `${this.name} організовує зустріч`;
}
}
const developer = new Developer("Андрій", 80000, "JavaScript");
const manager = new Manager("Марія", 100000, 6);
console.log(developer.describe());
console.log(developer.work());
console.log(developer.reviewCode());
console.log(manager.describe());
console.log(manager.work());
console.log(manager.organizeMeeting());У цьому прикладі:
Employee містить спільні властивості name, salary і метод describe();
Developer та Manager успадковують ці властивості й метод;
кожен дочірній клас перевизначає метод work();
кожен дочірній клас має власний метод.
Поліморфізм означає, що однаковий виклик методу може поводитися по-різному для різних об’єктів.
class Shape {
area() {
return 0;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
}
const shapes = [
new Rectangle(10, 5),
new Circle(3)
];
for (const shape of shapes) {
console.log(shape.area());
}Цикл не знає, яка саме фігура перед ним. Він лише викликає area(), а JavaScript використовує реалізацію, визначену конкретним дочірнім класом.
Оператор instanceof перевіряє, чи створений об’єкт за допомогою певного класу або класу, від якого він успадковує.
class Animal {}
class Cat extends Animal {}
const cat = new Cat();
console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal); // true
console.log(cat instanceof Object); // trueОскільки Cat успадковує Animal, об’єкт cat є екземпляром обох класів.
Також можна перевірити, чи є значення екземпляром класу:
class User {}
const user = new User();
console.log(user instanceof User); // true
console.log({} instanceof User); // falseСтатичні методи належать самому класу, а не його екземплярам. Дочірній клас також може успадковувати статичні методи батьківського класу.
class User {
static getRole() {
return "Користувач";
}
}
class Admin extends User {}
console.log(Admin.getRole()); // КористувачВикликати статичний метод через екземпляр не можна:
const admin = new Admin();
// admin.getRole(); // TypeErrorСтатичний метод викликається через назву класу:
console.log(User.getRole());
console.log(Admin.getRole());Власні класи можна створювати на основі вбудованих класів JavaScript, наприклад Array.
class NumberList extends Array {
sum() {
return this.reduce((total, number) => total + number, 0);
}
}
const numbers = new NumberList(10, 20, 30);
console.log(numbers.sum()); // 60
console.log(numbers.length); // 3
console.log(numbers.map(number => number * 2));
// NumberList(3) [20, 40, 60]NumberList зберігає поведінку масиву й отримує власний метод sum().
Успадкування корисне, коли між сутностями справді існує відношення «є різновидом»:
Dog є різновидом Animal;
Developer є різновидом Employee;
Circle є різновидом Shape.
Але іноді краще використовувати композицію — створювати об’єкти, які містять інші об’єкти або функції.
Наприклад, автомобіль не є двигуном, але має двигун:
class Engine {
start() {
return "Двигун запущено";
}
}
class Car {
constructor(engine) {
this.engine = engine;
}
start() {
return this.engine.start();
}
}
const car = new Car(new Engine());
console.log(car.start()); // Двигун запущеноКомпозиція допомагає уникати надто глибоких і складних ієрархій класів.
super() у конструкторіЯкщо дочірній клас має конструктор, але в ньому не викликати super(), звернення до this спричинить помилку:
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name) {
// Помилка: super() не викликано
// this.name = name;
}
}Правильний варіант:
class Child extends Parent {
constructor(name) {
super(name);
}
}this до super()У дочірньому конструкторі this не можна використовувати до виклику super():
class Child extends Parent {
constructor() {
// this.value = 10; // Помилка
super();
this.value = 10;
}
}Методи екземпляра викликаються через об’єкт:
class User {
greet() {
return "Привіт";
}
}
const user = new User();
console.log(user.greet());Виклик User.greet() буде помилковим, оскільки greet() не є статичним методом.
Ланцюжок із великої кількості базових і дочірніх класів ускладнює розуміння коду. Якщо клас успадковує лише невелику частину поведінки або успадкування не описує природний зв’язок між сутностями, варто розглянути композицію.
superВиклик super.method() потрібен, коли дочірній клас справді має розширити поведінку батьківського. Якщо метод повністю замінюється, достатньо створити власну реалізацію без зайвого виклику super.
extends створює дочірній клас на основі батьківського.
Дочірній клас успадковує методи та властивості батьківського класу.
super() викликає конструктор батьківського класу.
super.method() викликає метод батьківського класу.
Дочірні класи можуть додавати власні властивості й методи.
Перевизначення методів дає змогу змінювати поведінку дочірнього класу.
Поліморфізм дає змогу працювати з різними об’єктами через спільний інтерфейс.
instanceof перевіряє належність об’єкта до класу або його батьківського класу.
Успадкування варто використовувати для зв’язку «є різновидом», а для зв’язку «має» часто краще підходить композиція.