NodeJS Back-end Инженер

NodeJS Back-end Инженер

Роадмап навыков для прокачки

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

JavaScript (ES6 и новее)Классы в ES6Основное

Основная идея

Наследование классов в JavaScript позволяет создавать новые классы на основе существующих, переиспользуя и расширяя их функциональность. Реализуется через ключевое слово extends.

Ключевые аспекты

  • extends — ключевое слово для наследования от родительского класса
  • super() — вызов конструктора родителя (обязателен в дочернем конструкторе)
  • super.method() — вызов метода родительского класса
  • Переопределение методов — дочерний класс может заменить методы родителя
  • Цепочка прототипов — наследование работает через прототипы

Плюсы

  • Переиспользование кода без дублирования
  • Чёткая иерархия и организация классов
  • Возможность расширять и модифицировать поведение родителя
  • Полиморфизм — разные классы с единым интерфейсом

Минусы

  • Только одиночное наследование (нет множественного)
  • Глубокие иерархии усложняют понимание кода
  • Тесная связанность между родителем и потомком

Частые ошибки на собеседованиях

  • Забывают вызвать super() в конструкторе дочернего класса — это обязательно, если есть constructor
  • Обращаются к this до вызова super() — получают ReferenceError
  • Путают наследование с композицией и не знают, когда что использовать
  • Не понимают, что super нельзя использовать в стрелочных функциях

Введение и проблематика

При разработке часто возникает необходимость создавать объекты со схожим поведением, но с некоторыми отличиями. Например, у нас есть базовый класс Animal, а нам нужны Dog, Cat, Bird — каждый со своими особенностями, но с общей базой.

Наследование позволяет:

  • Избежать дублирования кода
  • Создать иерархию классов
  • Расширять функциональность без изменения исходного класса

В JavaScript наследование классов реализовано через прототипную цепочку. Ключевое слово extends — синтаксический сахар для настройки прототипов.


Базовая теория

Синтаксис extends

Code Example 1: Что произойдёт при вызове child.greet()? Откуда Child берёт этот метод?

js
class Parent {
  constructor(name) {
    this.name = name;
  }
 
  greet() {
    console.log(`Привет, я ${this.name}`);
  }
}
 
class Child extends Parent {
  // Child наследует всё от Parent
}
 
const child = new Child('Алексей');
child.greet(); // 'Привет, я Алексей'

Ключевое слово super

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

  1. Вызова конструктора родителяsuper()
  2. Вызова методов родителяsuper.methodName()

Code Example 2: Что выведет dog.speak()? Для чего используется super в конструкторе и в методе?

js
class Animal {
  constructor(name) {
    this.name = name;
  }
 
  speak() {
    console.log(`${this.name} издаёт звук`);
  }
}
 
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // вызов конструктора Animal
    this.breed = breed;
  }
 
  speak() {
    super.speak(); // вызов метода родителя
    console.log(`${this.name} лает`);
  }
}
 
const dog = new Dog('Рекс', 'Овчарка');
dog.speak();
// 'Рекс издаёт звук'
// 'Рекс лает'
🚫

Если дочерний класс имеет конструктор, вызов super() обязателен и должен быть до обращения к this.


Практические примеры

Создание иерархии классов

Code Example 3: Что выведут вызовы car.start(), car.getInfo(), bike.wheelie()? Как работает наследование методов start() и stop()?

Шаг 1: Создаём базовый класс

js
class Vehicle {
  constructor(brand, model) {
    this.brand = brand;
    this.model = model;
    this.isRunning = false;
  }
 
  start() {
    this.isRunning = true;
    console.log(`${this.brand} ${this.model} запущен`);
  }
 
  stop() {
    this.isRunning = false;
    console.log(`${this.brand} ${this.model} остановлен`);
  }
 
  getInfo() {
    return `${this.brand} ${this.model}`;
  }
}

Шаг 2: Создаём дочерний класс Car

js
class Car extends Vehicle {
  constructor(brand, model, doors) {
    super(brand, model);
    this.doors = doors;
    this.gear = 'P'; // parking
  }
 
  shift(gear) {
    this.gear = gear;
    console.log(`Переключено на ${gear}`);
  }
 
  getInfo() {
    return `${super.getInfo()}, ${this.doors} двери`;
  }
}

Шаг 3: Создаём дочерний класс Motorcycle

js
class Motorcycle extends Vehicle {
  constructor(brand, model, type) {
    super(brand, model);
    this.type = type; // sport, cruiser, touring
  }
 
  wheelie() {
    if (this.isRunning) {
      console.log(`${this.brand} делает вилли!`);
    } else {
      console.log('Сначала заведите мотоцикл');
    }
  }
}

Шаг 4: Используем классы

js
const car = new Car('Toyota', 'Camry', 4);
car.start();           // 'Toyota Camry запущен'
car.shift('D');        // 'Переключено на D'
console.log(car.getInfo()); // 'Toyota Camry, 4 двери'
 
const bike = new Motorcycle('Yamaha', 'R1', 'sport');
bike.start();          // 'Yamaha R1 запущен'
bike.wheelie();        // 'Yamaha делает вилли!'

Переопределение методов

Code Example 4: Что выведут rect.describe() и circle.describe()? Как работает полиморфизм в этом примере?

js
class Shape {
  constructor(color) {
    this.color = color;
  }
 
  getArea() {
    return 0; // базовая реализация
  }
 
  describe() {
    return `Фигура цвета ${this.color} с площадью ${this.getArea()}`;
  }
}
 
class Rectangle extends Shape {
  constructor(color, width, height) {
    super(color);
    this.width = width;
    this.height = height;
  }
 
  // Переопределяем метод
  getArea() {
    return this.width * this.height;
  }
}
 
class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }
 
  getArea() {
    return Math.PI * this.radius ** 2;
  }
}
 
const rect = new Rectangle('синий', 10, 5);
const circle = new Circle('красный', 7);
 
console.log(rect.describe());   // 'Фигура цвета синий с площадью 50'
console.log(circle.describe()); // 'Фигура цвета красный с площадью 153.93...'

Проверка типа через instanceof

Code Example 5: Что вернёт каждый instanceof? Как JavaScript определяет принадлежность к классу?

js
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
 
const dog = new Dog();
 
console.log(dog instanceof Dog);    // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Cat);    // false

Пограничные кейсы

⚠️

Обращение к this до super() вызывает ReferenceError.

Code Example 6: Почему обращение к this до вызова super() вызывает ошибку?

js
class Parent {
  constructor(name) {
    this.name = name;
  }
}
 
class Child extends Parent {
  constructor(name, age) {
    // this.age = age; // ReferenceError!
    super(name);
    this.age = age; // Правильно: после super()
  }
}
⚠️

super в стрелочных функциях работает из внешнего контекста.

Code Example 7: Что вернёт child.greet()? Почему super работает внутри стрелочной функции?

js
class Parent {
  greet() {
    return 'Привет от родителя';
  }
}
 
class Child extends Parent {
  greet() {
    // Стрелочная функция берёт super из окружающего метода
    const arrow = () => super.greet();
    return arrow() + ' и от ребёнка';
  }
}
 
const child = new Child();
console.log(child.greet()); // 'Привет от родителя и от ребёнка'

Наследование статических методов

Code Example 8: Что вернут Parent.create() и Child.create()? Как работает this в статическом методе?

js
class Parent {
  static create() {
    return new this();
  }
 
  static description = 'Базовый класс';
}
 
class Child extends Parent {
  static description = 'Дочерний класс';
}
 
const parent = Parent.create(); // экземпляр Parent
const child = Child.create();   // экземпляр Child
 
console.log(Parent.description); // 'Базовый класс'
console.log(Child.description);  // 'Дочерний класс'

Наследование vs Композиция

Code Example 9: В чём разница между этими двумя подходами? Когда лучше использовать каждый из них?

js
// Наследование: "is-a" (является)
class Animal {
  eat() { console.log('Ест'); }
}
 
class Dog extends Animal {
  bark() { console.log('Лает'); }
}
 
// Dog ЯВЛЯЕТСЯ Animal
const dog = new Dog();
dog.eat();  // унаследовано
dog.bark(); // собственный метод

Правило: «Предпочитайте композицию наследованию». Наследование создаёт жёсткую связь, композиция — гибкую.


Плюсы и минусы

АспектНаследованиеКомпозиция
СвязанностьВысокаяНизкая
ГибкостьОграничена иерархиейВысокая
ПереиспользованиеЧерез extendsЧерез внедрение объектов
ТестируемостьСложнееПроще
Когда использоватьЧёткая иерархия «is-a»Поведение «has-a»

Вопросы интервьюера

Q: Что произойдёт, если не вызвать super() в дочернем конструкторе?

ReferenceError: Must call super constructor before accessing 'this'. Если конструктор определён, super() обязателен.

Q: Можно ли наследовать от нескольких классов?

Нет, JavaScript поддерживает только одиночное наследование. Для множественного поведения используют миксины.

Q: Как работает super под капотом?

super ссылается на прототип родительского класса. super() эквивалентен Parent.call(this, args), а super.method()Parent.prototype.method.call(this).

Q: Когда использовать наследование, а когда композицию?

Наследование — когда есть чёткая связь «является» (Dog is Animal). Композиция — когда есть связь «имеет» (Car has Engine) или нужна гибкость.


Источники

Code Example 1: extends

❓ Что произойдёт при вызове child.greet()? Откуда Child берёт этот метод?

js
class Parent {
  constructor(name) {
    this.name = name;
  }
 
  greet() {
    console.log(`Привет, я ${this.name}`);
  }
}
 
class Child extends Parent {
}
 
const child = new Child('Алексей');
child.greet();

Code Example 2: super keyword

❓ Что выведет dog.speak()? Для чего используется super в конструкторе и в методе?

js
class Animal {
  constructor(name) {
    this.name = name;
  }
 
  speak() {
    console.log(`${this.name} издаёт звук`);
  }
}
 
class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
 
  speak() {
    super.speak();
    console.log(`${this.name} лает`);
  }
}
 
const dog = new Dog('Рекс', 'Овчарка');
dog.speak();

Code Example 3: Vehicle hierarchy

❓ Что выведут вызовы car.start(), car.getInfo(), bike.wheelie()? Как работает наследование методов start() и stop()?

js
class Vehicle {
  constructor(brand, model) {
    this.brand = brand;
    this.model = model;
    this.isRunning = false;
  }
 
  start() {
    this.isRunning = true;
    console.log(`${this.brand} ${this.model} запущен`);
  }
 
  stop() {
    this.isRunning = false;
    console.log(`${this.brand} ${this.model} остановлен`);
  }
 
  getInfo() {
    return `${this.brand} ${this.model}`;
  }
}
 
class Car extends Vehicle {
  constructor(brand, model, doors) {
    super(brand, model);
    this.doors = doors;
    this.gear = 'P';
  }
 
  shift(gear) {
    this.gear = gear;
    console.log(`Переключено на ${gear}`);
  }
 
  getInfo() {
    return `${super.getInfo()}, ${this.doors} двери`;
  }
}
 
class Motorcycle extends Vehicle {
  constructor(brand, model, type) {
    super(brand, model);
    this.type = type;
  }
 
  wheelie() {
    if (this.isRunning) {
      console.log(`${this.brand} делает вилли!`);
    } else {
      console.log('Сначала заведите мотоцикл');
    }
  }
}
 
const car = new Car('Toyota', 'Camry', 4);
car.start();
car.shift('D');
console.log(car.getInfo());
 
const bike = new Motorcycle('Yamaha', 'R1', 'sport');
bike.start();
bike.wheelie();

Code Example 4: Method overriding

❓ Что выведут rect.describe() и circle.describe()? Как работает полиморфизм в этом примере?

js
class Shape {
  constructor(color) {
    this.color = color;
  }
 
  getArea() {
    return 0;
  }
 
  describe() {
    return `Фигура цвета ${this.color} с площадью ${this.getArea()}`;
  }
}
 
class Rectangle extends Shape {
  constructor(color, width, height) {
    super(color);
    this.width = width;
    this.height = height;
  }
 
  getArea() {
    return this.width * this.height;
  }
}
 
class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }
 
  getArea() {
    return Math.PI * this.radius ** 2;
  }
}
 
const rect = new Rectangle('синий', 10, 5);
const circle = new Circle('красный', 7);
 
console.log(rect.describe());
console.log(circle.describe());

Code Example 5: instanceof

❓ Что вернёт каждый instanceof? Как JavaScript определяет принадлежность к классу?

js
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
 
const dog = new Dog();
 
console.log(dog instanceof Dog);
console.log(dog instanceof Animal);
console.log(dog instanceof Cat);

Code Example 6: this before super()

❓ Почему обращение к this до вызова super() вызывает ошибку?

js
class Parent {
  constructor(name) {
    this.name = name;
  }
}
 
class Child extends Parent {
  constructor(name, age) {
    // this.age = age;
    super(name);
    this.age = age;
  }
}

Code Example 7: super in arrow functions

❓ Что вернёт child.greet()? Почему super работает внутри стрелочной функции?

js
class Parent {
  greet() {
    return 'Привет от родителя';
  }
}
 
class Child extends Parent {
  greet() {
    const arrow = () => super.greet();
    return arrow() + ' и от ребёнка';
  }
}
 
const child = new Child();
console.log(child.greet());

Code Example 8: Static method inheritance

❓ Что вернут Parent.create() и Child.create()? Как работает this в статическом методе?

js
class Parent {
  static create() {
    return new this();
  }
 
  static description = 'Базовый класс';
}
 
class Child extends Parent {
  static description = 'Дочерний класс';
}
 
const parent = Parent.create();
const child = Child.create();
 
console.log(Parent.description);
console.log(Child.description);

Code Example 9: Inheritance vs Composition

❓ В чём разница между этими двумя подходами? Когда лучше использовать каждый из них?

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

js
class Animal {
  eat() { console.log('Ест'); }
}
 
class Dog extends Animal {
  bark() { console.log('Лает'); }
}
 
const dog = new Dog();
dog.eat();
dog.bark();

Композиция:

js
class Engine {
  start() { console.log('Двигатель запущен'); }
}
 
class Car {
  constructor() {
    this.engine = new Engine();
  }
 
  start() {
    this.engine.start();
    console.log('Машина готова к поездке');
  }
}
 
const car = new Car();
car.start();