Наследование и полиморфизм
Расширение классов через extends, переопределение методов и полиморфизм
Цели урока
- Понять концепцию наследования и зачем оно нужно
- Освоить ключевое слово extends и вызов super()
- Научиться переопределять методы в дочерних классах
- Понять полиморфизм — один интерфейс, разные реализации
- Разобрать цепочку наследования и её ограничения
- Изучить типичные ошибки при работе с наследованием
1. Зачем нужно наследование
Представьте, что вы пишете игру. У вас есть класс Enemy — враг. Но враги бывают разные: Goblin, Orc, Dragon. У каждого свои особенности, но у всех есть общее: имя, здоровье, метод атаки.
Без наследования вам пришлось бы копировать код для каждого типа врага. С наследованием вы пишете общую логику один раз в базовом классе, а дочерние классы только добавляют своё.
Наследование — как семья. Дети наследуют гены родителей (общие черты), но у каждого свои особенности. Сын может быть выше отца, дочь может иметь другой цвет глаз — но общее (нос, глаза, руки) передаётся по наследству.
Ключевое преимущество наследования — DRY (Don't Repeat Yourself): не повторяй себя. Общая логика живёт в родителе, специфичная — в наследнике.
2. extends и super — базовый синтаксис
Ключевое слово extends создаёт наследование: дочерний класс получает все свойства и методы родителя.
class Animal {
constructor(
public name: string,
protected energy: number = 100
) {}
eat(food: number): void {
this.energy += food;
console.log(`${this.name} поел. Энергия: ${this.energy}`);
}
sleep(): void {
this.energy += 50;
console.log(`${this.name} поспал. Энергия: ${this.energy}`);
}
}
// Дочерний класс (наследник)
class Dog extends Animal {
constructor(
name: string,
public breed: string
) {
// super() вызывает конструктор родителя
super(name);
}
// Свой метод — специфичный для Dog
fetch(): void {
this.energy -= 15;
console.log(`${this.name} побегал за палкой. Энергия: ${this.energy}`);
}
}
Использование:
// Dog НАСЛЕДУЕТ методы от Animal
dog.eat(30); // ✅ Метод родителя
dog.sleep(); // ✅ Метод родителя
dog.fetch(); // ✅ Свой метод
console.log(dog.name); // "Шарик"
console.log(dog.breed); // "Доберман"
Вызов super() в конструкторе дочернего класса обязателен до любого обращения к this. Если не вызвать super(), TypeScript выдаст ошибку: 'super()' must be called before accessing 'this'.
super() может передавать аргументы конструктору родителя:
constructor(
public name: string,
protected energy: number
) {}
}
class Cat extends Animal {
constructor(
name: string,
public indoor: boolean
) {
// Передаём name родителю, а energy задаём по умолчанию
super(name, 80);
}
purr(): void {
console.log(`${this.name} мурлычет`);
}
}
const cat = new Cat("Мурка", true);
cat.eat(20); // ✅ Метод родителя
cat.purr(); // "Мурка мурлычет"
3. Переопределение методов (Override)
Дочерний класс может переопределить метод родителя — заменить его реализацию на свою. При этом метод остаётся с тем же именем и сигнатурой:
constructor(public color: string) {}
area(): number {
return 0; // Базовая реализация
}
describe(): string {
return `Фигура площадью ${this.area()}`;
}
}
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}
// Переопределяем area()
area(): number {
return Math.PI * this.radius ** 2;
}
// Переопределяем describe()
describe(): string {
return `${this.color} круг с радиусом ${this.radius}, площадь: ${this.area().toFixed(2)}`;
}
}
class Rectangle extends Shape {
constructor(
color: string,
public width: number,
public height: number
) {
super(color);
}
area(): number {
return this.width * this.height;
}
describe(): string {
return `${this.color} прямоугольник ${this.width}×${this.height}, площадь: ${this.area()}`;
}
}
Ключевое слово override (TypeScript 4.3+) — явное указание, что метод переопределяет родительский:
// override защищает от опечаток в имени метода
override area(): number {
return Math.PI * this.radius ** 2;
}
// Если написать areа() (с опечаткой) — override выдаст ошибку
// override areа(): number { ... } // ❌ Method 'areа' is not declared in class
}
4. Полиморфизм — один интерфейс, много реализаций
Полиморфизм (от греч. «много форм») — это возможность treats объектов разных классов через общий интерфейс. Вы вызываете один и тот же метод, а он ведёт себя по-разному в зависимости от типа объекта.
Представьте кнопку «Включить» на пульте. Вы нажимаете одну кнопку, но TV включает телевизор, DVD — проигрыватель, AC — кондиционер. Одно действие — разные результаты в зависимости от объекта.
// Но работает с любым наследником!
function printArea(shape: Shape): void {
console.log(`Площадь: ${shape.area()}`);
}
// Разные объекты — одно и то же действие
const circle = new Circle("красный", 5);
const rect = new Rectangle("синий", 10, 5);
printArea(circle); // "Площадь: 78.54" — вызывается Circle.area()
printArea(rect); // "Площадь: 50" — вызывается Rectangle.area()
Без полиморфизма вам пришлось бы проверять тип каждого объекта:
function printAreaBad(shape: Circle | Rectangle) {
if (shape instanceof Circle) {
console.log(`Площадь круга: ${shape.area()}`);
} else if (shape instanceof Rectangle) {
console.log(`Площадь прямоугольника: ${shape.area()}`);
}
}
// ✅ С полиморфизмом — просто вызываем метод
function printAreaGood(shape: Shape) {
console.log(`Площадь: ${shape.area()}`);
}
Полиморфизм позволяет писать код, который работает с любым количеством новых наследников без изменений:
class Triangle extends Shape {
constructor(color: string, public base: number, public h: number) {
super(color);
}
area(): number { return 0.5 * this.base * this.h; }
}
// printArea() работает с Triangle без изменений!
printArea(new Triangle("зелёный", 10, 6)); // "Площадь: 30"
5. Проверка типа через instanceof
Иногда нужно проверить, к какому конкретному классу относится объект. Для этого используется instanceof:
if (shape instanceof Circle) {
// TypeScript ЗНАЕТ, что shape — это Circle
// Можно обращаться к shape.radius
return `Круг радиусом ${shape.radius}`;
} else if (shape instanceof Rectangle) {
// TypeScript ЗНАЕТ, что shape — это Rectangle
return `Прямоугольник ${shape.width}×${shape.height}`;
} else {
return "Неизвестная фигура";
}
}
Постарайтесь избегать instanceof — это признак нарушения полиморфизма. Если вам нужно проверять тип, возможно, стоит добавить метод в родительский класс или использовать другие паттерны.
6. Цепочка наследования
Наследование может быть многоуровневым: класс A наследует от B, B от C, и так далее.
class LivingThing {
constructor(public name: string) {}
breathe(): void { console.log(`${this.name} дышит`); }
}
// Наследник LivingThing
class Animal extends LivingThing {
constructor(name: string, public legs: number) {
super(name);
}
walk(): void { console.log(`${this.name} идёт`); }
}
// Наследник Animal
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name, 4);
}
bark(): void { console.log("Гав-гав!"); }
}
// Dog имеет ВСЕ методы из всех уровней!
const dog = new Dog("Рекс", "Овчарка");
dog.breathe(); // ✅ из LivingThing
dog.walk(); // ✅ из Animal
dog.bark(); // ✅ из Dog
Не увлекайтесь глубокими цепочками наследования. В реальных проектах рекомендуется не более 2-3 уровней. Глубокое наследование делает код сложным и хрупким. Часто лучше использовать композицию (встраивание объектов) вместо наследования.
7. Практический пример — Иерархия сотрудников
protected bonus: number = 0;
constructor(
public name: string,
protected baseSalary: number
) {}
calculateSalary(): number {
return this.baseSalary + this.bonus;
}
getInfo(): string {
return `${this.name}: ${this.calculateSalary()} руб.`;
}
}
// Менеджер — получает бонус за управление
class Manager extends Employee {
override calculateSalary(): number {
this.bonus = this.baseSalary * 0.3;
return super.calculateSalary();
}
manage(): void {
console.log(`${this.name} управляет командой`);
}
}
// Разработчик — получает бонус за код
class Developer extends Employee {
constructor(
name: string,
baseSalary: number,
public language: string
) {
super(name, baseSalary);
}
override calculateSalary(): number {
this.bonus = 5000;
return super.calculateSalary();
}
code(): void {
console.log(`${this.name} пишет код на ${this.language}`);
}
}
Использование полиморфизма:
const team: Employee[] = [
new Manager("Анна", 150000),
new Developer("Борис", 120000, "TypeScript"),
new Developer("Вика", 130000, "Python"),
];
// Один цикл — разные результаты!
team.forEach(emp => {
console.log(emp.getInfo());
});
// "Анна: 195000 руб."
// "Борис: 125000 руб."
// "Вика: 135000 руб."
8. Частые ошибки
Ошибка 1: Забыли super() в конструкторе
class Dog extends Animal {
constructor(name: string) {
// ❌ super() забыли!
this.name = name; // ❌ 'this' not allowed before super()
}
}
// ✅ Правильно:
class Dog2 extends Animal {
constructor(name: string) {
super(name); // ✅ Сначала super()
}
}
Ошибка 2: Обращение к private свойству родителя
private secret = "hidden";
}
class Child extends Parent {
show() {
// console.log(this.secret); // ❌ Property 'secret' is private
}
}
// ✅ Решение: используйте protected вместо private
Ошибка 3: Переопределили метод, изменив сигнатуру
class Base {
greet(name: string): string {
return `Привет, ${name}`;
}
}
class Child2 extends Base {
// ❌ Изменили количество параметров!
greet(name: string, title: string): string {
return `${title} ${name}`;
}
}
Практическое задание
Создайте иерархию классов для транспорта:
Vehicle— базовый: brand, year, start(), stop()Car extends Vehicle— additional: doors, isElectric, openTrunk()Bicycle extends Vehicle— additional: hasBell, ringBell()- Переопределите start() в каждом классе с разным сообщением
- Создайте массив Vehicle[] и пройдитесь по нему циклом, вызывая start()
Чек-лист урока
- extends — создаёт наследование: class Child extends Parent
- super() — вызывает конструктор родителя, обязателен в конструкторе наследника
- super.method() — вызывает метод родителя (полезно при переопределении)
- Override — явное указание, что метод переопределяет родительский
- Полиморфизм — объекты разных классов обрабатываются через общий интерфейс
- instanceof — проверка, является ли объект экземпляром класса
- Цепочка наследования — A extends B extends C (рекомендуется не более 2-3 уровней)
- Protected — для свойств, доступных наследникам
- Private не наследуется — наследники не могут обращаться к private свойствам
- DRY — общая логика в родителе, специфичная — в наследнике
- Частые ошибки — забыли super(), обращение к private, несовпадение сигнатур
Тест по наследованию и полиморфизму
8 вопросов