Наследование: extends и super
Наследование — механизм, позволяющий одному классу взять все свойства и методы другого класса. Это как гены: дочерний класс получает «ДНК» родителя, но может и добавить что-то своё, и переопределить унаследованное.
🤔 Зачем нужно наследование?
Представьте: вы делаете игру с животными. Нужны классы Dog, Cat, Bird. У каждого есть имя, возраст, метод eat(). Без наследования придётся дублировать один и тот же код в трёх классах:
// БЕЗ наследования — дублирование кода!
class Dog {
constructor(name) { this.name = name; }
eat() { console.log(this.name + " ест."); }
bark() { console.log("Гав!"); }
}
class Cat {
constructor(name) { this.name = name; } // дубль!
eat() { console.log(this.name + " ест."); } // дубль!
meow() { console.log("Мяу!"); }
}
Это нарушает принцип DRY (Don't Repeat Yourself — Не повторяй себя). Если нужно изменить метод eat() — придётся менять в трёх местах. Наследование решает это: выносим общую логику в родительский класс Animal.
🧬 Наследование — механизм, при котором дочерний класс автоматически получает все свойства и методы родительского класса. Пишешь общий код один раз в родителе — все потомки его наследуют.
🔗 Ключевое слово extends
Создадим родительский класс Animal и унаследуем от него Dog и Cat:
// Родительский класс (суперкласс, базовый класс)
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() {
console.log(`${this.name} ест.`);
}
sleep() {
console.log(`${this.name} спит.`);
}
describe() {
return `${this.name}, возраст: ${this.age} лет`;
}
}
// Дочерний класс — extends Animal
class Dog extends Animal {
bark() {
console.log(`${this.name} говорит: Гав!`);
}
fetch() {
console.log(`${this.name} приносит мячик!`);
}
}
class Cat extends Animal {
meow() {
console.log(`${this.name} говорит: Мяу!`);
}
purr() {
console.log(`${this.name} мурлычет...`);
}
}
const rex = new Dog("Рекс", 3);
const murka = new Cat("Мурка", 5);
rex.eat(); // Рекс ест. ← унаследовано от Animal!
rex.bark(); // Рекс говорит: Гав! ← своё
rex.sleep(); // Рекс спит. ← унаследовано
murka.eat(); // Мурка ест. ← унаследовано от Animal!
murka.meow(); // Мурка говорит: Мяу! ← своё
murka.purr(); // Мурка мурлычет... ← своё
console.log(rex.describe()); // Рекс, возраст: 3 лет
Смотрите как красиво: Dog и Cat получили все методы Animal — eat(), sleep(), describe() — бесплатно! И каждый добавил только своё уникальное поведение.
⬆️ super() в конструкторе: обязательный вызов
Заметили, что в Dog и Cat выше нет конструктора? Если конструктор не написан, JavaScript использует конструктор родителя автоматически. Но что если дочернему классу нужен свой конструктор с дополнительными параметрами?
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() {
console.log(`${this.name} ест.`);
}
}
class Dog extends Animal {
constructor(name, age, breed) { // добавляем параметр breed
super(name, age); // ОБЯЗАТЕЛЬНО! Вызывает конструктор Animal
this.breed = breed; // только после super можно использовать this!
}
describe() {
return `${this.name} (${this.breed}), ${this.age} лет`;
}
}
const rex = new Dog("Рекс", 3, "Немецкая овчарка");
console.log(rex.describe()); // Рекс (Немецкая овчарка), 3 лет
rex.eat(); // Рекс ест. — унаследовано!
🚨 Правило! Если в дочернем классе есть конструктор, первой строкой ОБЯЗАТЕЛЬНО должен быть super(). Иначе — ошибка. До вызова super() нельзя использовать this.
Почему super() обязателен? Потому что super() вызывает конструктор родителя, который инициализирует унаследованные свойства (name, age). Без этого объект не готов к работе.
🔄 super.method(): вызов родительского метода
super — это не только вызов конструктора. Через super.methodName() можно вызвать любой метод родительского класса прямо из дочернего:
class Animal {
describe() {
return `Животное: ${this.name}`;
}
}
class Dog extends Animal {
describe() {
// Вызываем родительский describe() и расширяем его
const base = super.describe();
return `${base}, порода: ${this.breed}, говорит: Гав!`;
}
}
const rex = new Dog("Рекс", 3, "Лабрадор");
console.log(rex.describe());
// Животное: Рекс, порода: Лабрадор, говорит: Гав!
Это паттерн «расширение» родительского поведения: берём результат родительского метода и добавляем что-то своё.
✏️ Переопределение (override) методов
Дочерний класс может полностью переопределить (override) унаследованный метод — написать свою версию с тем же именем:
class Animal {
sound() {
console.log(`${this.name} издаёт звук.`);
}
}
class Dog extends Animal {
sound() {
// Полностью переопределяем — родительский код не вызывается
console.log(`${this.name} говорит: Гав-гав!`);
}
}
class Cat extends Animal {
sound() {
console.log(`${this.name} говорит: Мяу-мяу!`);
}
}
class Bird extends Animal {
// Нет переопределения sound() — используется родительский метод
}
const rex = new Dog("Рекс", 2);
const murka = new Cat("Мурка", 4);
const tweety = new Bird("Твити", 1);
rex.sound(); // Рекс говорит: Гав-гав!
murka.sound(); // Мурка говорит: Мяу-мяу!
tweety.sound(); // Твити издаёт звук. ← родительский метод
JavaScript при вызове rex.sound() смотрит: есть ли метод sound в Dog? Есть — используй его. У Bird нет своего sound — используй родительский из Animal. Это называется полиморфизм — разные объекты реагируют на один и тот же вызов по-своему.
⛓️ Цепочка наследования
Наследование можно выстраивать в цепочку: класс наследует от класса, который наследует от другого класса:
class LivingThing {
breathe() { console.log("Дышу..."); }
}
class Animal extends LivingThing {
constructor(name) { super(); this.name = name; }
eat() { console.log(this.name + " ест."); }
}
class Dog extends Animal {
bark() { console.log(this.name + ": Гав!"); }
}
const rex = new Dog("Рекс");
rex.breathe(); // Дышу... ← из LivingThing
rex.eat(); // Рекс ест. ← из Animal
rex.bark(); // Рекс: Гав! ← свой
Dog наследует от Animal, который наследует от LivingThing. Все методы всей цепочки доступны в Dog. Но не увлекайтесь — глубокие цепочки (больше 3 уровней) сложно поддерживать.
🔍 instanceof: проверка принадлежности
Оператор instanceof проверяет, является ли объект экземпляром класса — и учитывает всю цепочку наследования:
const rex = new Dog("Рекс", 3, "Лабрадор");
console.log(rex instanceof Dog); // true — прямой класс
console.log(rex instanceof Animal); // true — родительский!
console.log(rex instanceof Cat); // false — другая ветка
Это полезно, когда функция принимает разные типы и нужно понять, с каким работать.
⚖️ Когда наследование нужно, а когда — нет
Наследование — мощный инструмент, но не всегда лучший выбор. Есть альтернатива — composition (композиция): вместо «быть» используй «иметь».
- Отношение «является» (is-a): собака является животным
- Дочерние классы реально расширяют родителя
- Общий код нужно писать один раз
- Отношение «имеет» (has-a): автомобиль имеет двигатель
- Нужно собрать объект из нескольких независимых возможностей
- Цепочка наследования уходит глубже трёх уровней
// Composition — вместо наследования используем поле
class Engine {
start() { console.log("Двигатель запущен"); }
stop() { console.log("Двигатель выключен"); }
}
class Car {
constructor() {
this.engine = new Engine(); // Engine не родитель — он часть Car
}
drive() {
this.engine.start();
console.log("Едем!");
}
}
const car = new Car();
car.drive(); // Двигатель запущен / Едем!
✅ Итоги урока
- extends — ключевое слово наследования.
class Dog extends Animal— Dog получает все методы Animal - super(args) в конструкторе — обязательный вызов конструктора родителя. Первая строка конструктора дочернего класса
- super.method() — вызов родительского метода из дочернего класса
- Override — переопределение: метод с тем же именем в дочернем классе замещает родительский
- Цепочка наследования: Dog → Animal → LivingThing. Все методы всей цепочки доступны
- instanceof — проверка принадлежности, учитывает цепочку наследования
- Наследование — для отношений «является». Composition — для «имеет». Не злоупотребляй наследованием
В следующем уроке — прототипы: как ООП работает под капотом JavaScript. Объяснение механизма, который стоит за всем этим. 🚀
Наследование: extends и super
7 вопросов