Наследование

Наследование — механизм, позволяющий одному объекту/классу получить свойства и методы другого. В JavaScript реализуется через прототипную цепочку и синтаксис extends/super.

Зачем нужно

Наследование позволяет переиспользовать код, создавать иерархии типов и расширять существующую функциональность без дублирования. Понимание наследования в JS критично для работы с фреймворками и библиотеками.

Где используется

Иерархии компонентов (React), модели данных (ORM), расширение ошибок, игровые объекты, UI-виджеты, паттерны проектирования.

Предпосылки

Прототипы, Классы, this

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

Практика

  1. Создай базовый класс Shape и наследников Circle, Rectangle с методом area
  2. Реализуй CustomError extends Error с дополнительными полями
  3. Напиши иерархию через прототипы (без class) и сравни с классами
  4. Создай миксин Loggable и примени к классу
  5. Реализуй паттерн 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», предпочитайте агрегацию или композицию.

Связанные темы

Источники