Наследование
Наследование — механизм, позволяющий одному объекту/классу получить свойства и методы другого. В JavaScript реализуется через прототипную цепочку и синтаксис
extends/super.
Зачем нужно
Наследование позволяет переиспользовать код, создавать иерархии типов и расширять существующую функциональность без дублирования. Понимание наследования в JS критично для работы с фреймворками и библиотеками.
Где используется
Иерархии компонентов (React), модели данных (ORM), расширение ошибок, игровые объекты, UI-виджеты, паттерны проектирования.
Предпосылки
Class extends
class Animal {
constructor(name) {
this.name = name;
this.energy = 100;
}
eat(amount) {
this.energy += amount;
console.log(`${this.name} ест. Энергия: ${this.energy}`);
}
sleep {
this.energy += 20;
console.log(`${this.name} спит. Энергия: ${this.energy}`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Вызов конструктора родителя (ОБЯЗАТЕЛЬНО)
this.breed = breed; // Собственное свойство
}
bark {
this.energy -= 5;
console.log(`${this.name} лает! Гав! Энергия: ${this.energy}`);
}
}
const rex = new Dog('Рекс', 'Овчарка');
rex.eat(10); // "Рекс ест. Энергия: 110" — метод Animal
rex.bark; // "Рекс лает! Гав! Энергия: 105" — метод Dog
rex.sleep; // "Рекс спит. Энергия: 125" — метод Animal
super
В конструкторе
class Shape {
constructor(color) {
this.color = color;
}
}
class Circle extends Shape {
constructor(color, radius) {
// super ДОЛЖЕН быть до обращения к this
super(color);
this.radius = radius;
}
get area {
return Math.PI * this.radius ** 2;
}
}
const c = new Circle('red', 5);
console.log(c.color); // "red"
console.log(c.area); // 78.54
В методах
class Animal {
speak {
return `${this.name} издаёт звук`;
}
}
class Cat extends Animal {
constructor(name) {
super(name || 'Кот');
this.name = name || 'Кот';
}
speak {
// Вызываем метод родителя
const base = super.speak;
return `${base}: Мяу!`;
}
}
const cat = new Cat('Мурка');
console.log(cat.speak); // "Мурка издаёт звук: Мяу!"
Переопределение методов (Override)
class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
error(message) {
console.error(`[ERROR] ${message}`);
}
}
class TimestampLogger extends Logger {
log(message) {
// Полное переопределение
const timestamp = new Date.toISOString();
console.log(`[${timestamp}] ${message}`);
}
error(message) {
// Расширение через super
super.error(`${new Date.toISOString()} - ${message}`);
}
}
Наследование через прототипы (до ES6)
function Vehicle(type, speed) {
this.type = type;
this.speed = speed;
}
Vehicle.prototype.describe = function {
return `${this.type}, скорость: ${this.speed}`;
};
function Car(brand, speed) {
Vehicle.call(this, 'Автомобиль', speed); // super аналог
this.brand = brand;
}
// Устанавливаем прототипную цепочку
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
Car.prototype.honk = function {
return `${this.brand}: Бип-бип!`;
};
const tesla = new Car('Tesla', 200);
console.log(tesla.describe); // "Автомобиль, скорость: 200"
console.log(tesla.honk); // "Tesla: Бип-бип!"
console.log(tesla instanceof Car); // true
console.log(tesla instanceof Vehicle); // true
Object.create наследование
const animalProto = {
init(name) {
this.name = name;
return this;
},
speak {
return `${this.name} говорит`;
}
};
const dogProto = Object.create(animalProto);
dogProto.bark = function {
return `${this.name}: Гав!`;
};
const myDog = Object.create(dogProto).init('Бобик');
console.log(myDog.speak); // "Бобик говорит"
console.log(myDog.bark); // "Бобик: Гав!"
Миксины
JavaScript не поддерживает множественное наследование, но миксины позволяют добавлять функциональность из нескольких источников:
const Serializable = {
toJSON {
return JSON.stringify(this);
},
fromJSON(json) {
return Object.assign(Object.create(this), JSON.parse(json));
}
};
const EventEmitter = {
_events: null,
on(event, handler) {
if (!this._events) this._events = {};
(this._events[event] ||= ).push(handler);
return this;
},
emit(event, ...args) {
(this._events?.[event] || ).forEach(h => h(...args));
return this;
}
};
// Применение миксинов
class User {
constructor(name) {
this.name = name;
}
}
Object.assign(User.prototype, Serializable, EventEmitter);
const user = new User('Иван');
user.on('greet', () => console.log('Привет!'));
user.emit('greet'); // "Привет!"
Наследование встроенных классов
class CustomArray extends Array {
get first {
return this[0];
}
get last {
return this[this.length - 1];
}
sum {
return this.reduce((a, b) => a + b, 0);
}
}
const arr = new CustomArray(1, 2, 3, 4, 5);
console.log(arr.first); // 1
console.log(arr.last); // 5
console.log(arr.sum); // 15
console.log(arr.map(x => x * 2)); // CustomArray [2, 4, 6, 8, 10]
// Расширение Error
class HttpError extends Error {
constructor(status, message) {
super(message);
this.name = 'HttpError';
this.status = status;
}
}
try {
throw new HttpError(404, 'Страница не найдена');
} catch (err) {
if (err instanceof HttpError) {
console.log(`HTTP ${err.status}: ${err.message}`);
}
}
Статическое наследование
class Parent {
static create(...args) {
return new this(...args);
}
static description = 'Родительский класс';
}
class Child extends Parent {
constructor(name) {
super;
this.name = name;
}
}
const child = Child.create('Иван'); // new Child('Иван')
console.log(child.name); // "Иван"
console.log(Child.description); // "Родительский класс" — наследуется
Частые ошибки
1. Забытый super в конструкторе
class Child extends Parent {
constructor {
// this.value = 1; // ReferenceError! super не вызван
super; // Обязательно перед обращением к this
this.value = 1;
}
}
2. Глубокая иерархия (> 3 уровней)
// Анти-паттерн: слишком глубокая иерархия
// Animal → Mammal → Domestic → Dog → GuideDog
// Предпочитайте композицию:
class Dog {
constructor(name) {
this.name = name;
this.walker = new Walker(); // композиция
this.feeder = new Feeder(); // вместо наследования
}
}
3. Неправильная проверка типа
class A {}
class B extends A {}
const b = new B();
// instanceof проверяет всю цепочку
console.log(b instanceof B); // true
console.log(b instanceof A); // true
// Для точной проверки
console.log(b.constructor === B); // true
console.log(b.constructor === A); // false
Практика
- Создай базовый класс
Shapeи наследниковCircle,Rectangleс методомarea - Реализуй
CustomError extends Errorс дополнительными полями - Напиши иерархию через прототипы (без class) и сравни с классами
- Создай миксин
Loggableи примени к классу - Реализуй паттерн Template Method через наследование
Ключевые тезисы
- Только расширение, не изменение. Поведение наследуется и расширяется, но не должно меняться (LSP, см. GRASP/SOLID). Меняем — ломаем подстановку.
superпопулирует поля предка.class Square extends Rectсsuper(side, side)заполняетwidthиheightбазового класса при том, чтоSquareпринимает один аргумент.- Класс наследует и статику. В
class extends:Square.__proto__ === Rect(неFunction.prototype). ПоэтомуRect.fromдоступен какSquare.from. - Глубокие иерархии замедляют чтение. 5-7 уровней — антипаттерн. См. Антипаттерны ООП.
- Композиция вместо наследования. Если связь не «is-a», предпочитайте агрегацию или композицию.
Связанные темы
- Прототипы
- Прототипная цепочка
- Классы
- Инкапсуляция
- Полиморфизм
- Конструкторы
- Пять способов наследования
- Ассоциация, агрегация, композиция
- Наследование vs композиция
Источники
- Timur · ООП: наследование и полиморфизм в JavaScript (2020-03-03, 29 мин)
- Timur · Прототипное программирование и прототипное наследование (2019-11-19, 37 мин) — пять способов сцепления
- Timur · Object Association, Aggregation, and Composition (2019-10-31)
- Timur · Have Objects Failed? Or What's Wrong With OOP (2019-05-23) — критика наследования
- MDN — extends
- JavaScript.info — Наследование классов