$ sudo teach IT
РАЗДЕЛ 8 · УРОК 4

Наследование: extends и super

Наследование — механизм, позволяющий одному классу взять все свойства и методы другого класса. Это как гены: дочерний класс получает «ДНК» родителя, но может и добавить что-то своё, и переопределить унаследованное.

⏱ ~30 минут 🎓 Для новичков ⚡ JavaScript

🤔 Зачем нужно наследование?

Представьте: вы делаете игру с животными. Нужны классы 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 вопросов

Наследование Vehicle → Car

// Создайте класс Vehicle с: // - конструктором (brand, year) // - методом getInfo(), возвращающим "brand (year)" // // Создайте класс Car extends Vehicle с: // - дополнительным параметром fuelType // - переопределённым методом getInfo(), который // вызывает родительский и добавляет ", fuel: fuelType" // Ваш код здесь

Классы Shape и Rectangle с переопределением

// Создайте класс Shape: // - метод getArea() возвращает 0 // // Создайте класс Rectangle extends Shape с: // - конструктором (width, height) // - переопределённым методом getArea(), // возвращающим width * height // Ваш код здесь