← JavaScript/Наследование классов#81 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

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

Какую проблему решает наследование

Представь, что ты разрабатываешь интернет-магазин. У тебя есть товары разных типов: книги, электроника, одежда. У всех есть общие поля — название, цена, количество. Но у каждого типа есть и свои: у книг — автор, у электроники — гарантия, у одежды — размер.

Без наследования придётся дублировать одни и те же методы (добавить в корзину, показать цену) в каждом классе. Наследование позволяет описать общее один раз, а в подклассах добавить только специфическое.

На основе предыдущих уроков

  • «Классы» — синтаксис class, constructor, методы экземпляра
  • «Прототипы» — как методы хранятся в prototype-цепочке
  • «this» — почему this важен в конструкторе
  • extends и super

    class Product {
      constructor(name, price) {
        this.name = name
        this.price = price
      }
    
      getInfo() {
        return `${this.name} — ${this.price} ₽`
      }
    }
    
    class Book extends Product {
      constructor(name, price, author) {
        super(name, price)   // ОБЯЗАТЕЛЬНО первым! Иначе ReferenceError
        this.author = author
      }
    
      getInfo() {
        return super.getInfo() + ` (автор: ${this.author})`
      }
    }
    
    const book = new Book('Чистый код', 1200, 'Роберт Мартин')
    console.log(book.getInfo())
    // 'Чистый код — 1200 ₽ (автор: Роберт Мартин)'

    Правило: в конструкторе дочернего класса super() должен вызываться до любого обращения к this. Нарушение — ReferenceError.

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

    Дочерний класс может полностью заменить метод родителя или дополнить его:

    class Electronics extends Product {
      constructor(name, price, warranty) {
        super(name, price)
        this.warranty = warranty  // гарантия в месяцах
      }
    
      // Полное переопределение
      getInfo() {
        return `${this.name} — ${this.price} ₽, гарантия ${this.warranty} мес.`
      }
    
      // Новый метод только для Electronics
      isUnderWarranty(monthsOld) {
        return monthsOld < this.warranty
      }
    }

    Полиморфизм

    Разные объекты реагируют по-своему на один и тот же вызов метода. Это позволяет писать универсальный код:

    const cart = [
      new Book('JavaScript', 990, 'Кайл Симпсон'),
      new Electronics('MacBook Pro', 180000, 24),
      new Product('Карандаш', 50),
    ]
    
    // Один и тот же метод — разный результат у каждого объекта
    cart.forEach(item => console.log(item.getInfo()))
    // 'JavaScript — 990 ₽ (автор: Кайл Симпсон)'
    // 'MacBook Pro — 180000 ₽, гарантия 24 мес.'
    // 'Карандаш — 50 ₽'

    instanceof — проверка типа

    const laptop = new Electronics('HP Laptop', 60000, 12)
    
    console.log(laptop instanceof Electronics)  // true
    console.log(laptop instanceof Product)      // true  — Electronics наследует Product
    console.log(laptop instanceof Book)         // false

    Типичные ошибки

    1. Забыли вызвать super() в конструкторе:

    // Сломано:
    class Book extends Product {
      constructor(name, price, author) {
        this.author = author  // ReferenceError: Must call super before accessing 'this'
        super(name, price)
      }
    }
    
    // Исправлено:
    class Book extends Product {
      constructor(name, price, author) {
        super(name, price)   // сначала super
        this.author = author
      }
    }

    2. Не вернули расширение из super.method():

    // Сломано — потеряли строку родителя:
    class Book extends Product {
      getInfo() {
        super.getInfo()  // результат проигнорирован!
        return `автор: ${this.author}`
      }
    }
    
    // Исправлено:
    class Book extends Product {
      getInfo() {
        return super.getInfo() + ` (автор: ${this.author})`
      }
    }

    3. Слишком глубокая иерархия:

    // Запах кода — 4+ уровня наследования трудно отлаживать
    class A extends B {}
    class C extends A {}
    class D extends C {}   // уже сложно понять откуда что пришло

    Если иерархия глубже 2-3 уровней — рассмотри композицию вместо наследования.

    В реальных проектах

  • React: class MyComponent extends React.Component — все классовые компоненты наследуют базовый класс React
  • Node.js: class CustomStream extends Readable — расширение встроенных потоков
  • ORM (Prisma, TypeORM): модели наследуют базовый класс с методами save/delete
  • Кастомные ошибки: class NotFoundError extends Error
  • Примеры

    Иерархия товаров интернет-магазина: Product, Book, Electronics

    class Product {
      constructor(name, price, stock = 0) {
        this.name = name
        this.price = price
        this.stock = stock
      }
    
      getInfo() {
        return `${this.name} — ${this.price.toLocaleString('ru-RU')} ₽`
      }
    
      isAvailable() {
        return this.stock > 0
      }
    
      toString() {
        return `[${this.constructor.name}: ${this.name}]`
      }
    }
    
    class Book extends Product {
      constructor(name, price, author, pages, stock) {
        super(name, price, stock)
        this.author = author
        this.pages = pages
      }
    
      getInfo() {
        return super.getInfo() + ` | ${this.author}, ${this.pages} стр.`
      }
    }
    
    class Electronics extends Product {
      constructor(name, price, brand, warranty, stock) {
        super(name, price, stock)
        this.brand = brand
        this.warranty = warranty
      }
    
      getInfo() {
        return super.getInfo() + ` | ${this.brand}, гарантия ${this.warranty} мес.`
      }
    
      isUnderWarranty(monthsOld) {
        return monthsOld <= this.warranty
      }
    }
    
    // Полиморфизм: один код для разных типов
    const catalog = [
      new Book('Чистый код', 1200, 'Роберт Мартин', 431, 5),
      new Book('JavaScript: Хорошие части', 890, 'Дуглас Крокфорд', 176, 0),
      new Electronics('iPhone 15', 89990, 'Apple', 12, 10),
      new Electronics('AirPods Pro', 24990, 'Apple', 6, 3),
    ]
    
    console.log('=== Каталог товаров ===')
    catalog.forEach(item => {
      const status = item.isAvailable() ? 'в наличии' : 'нет в наличии'
      console.log(item.getInfo() + ` [${status}]`)
    })
    // 'Чистый код — 1 200 ₽ | Роберт Мартин, 431 стр. [в наличии]'
    // 'JavaScript: Хорошие части — 890 ₽ | Дуглас Крокфорд, 176 стр. [нет в наличии]'
    // 'iPhone 15 — 89 990 ₽ | Apple, гарантия 12 мес. [в наличии]'
    // 'AirPods Pro — 24 990 ₽ | Apple, гарантия 6 мес. [в наличии]'
    
    // instanceof — проверяем типы
    const phone = catalog[2]
    console.log(phone instanceof Electronics)  // true
    console.log(phone instanceof Product)      // true
    console.log(phone instanceof Book)         // false
    
    // Метод только у Electronics
    if (phone instanceof Electronics) {
      console.log('Гарантия действует 3 месяца?', phone.isUnderWarranty(3))  // true
    }

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

    Какую проблему решает наследование

    Представь, что ты разрабатываешь интернет-магазин. У тебя есть товары разных типов: книги, электроника, одежда. У всех есть общие поля — название, цена, количество. Но у каждого типа есть и свои: у книг — автор, у электроники — гарантия, у одежды — размер.

    Без наследования придётся дублировать одни и те же методы (добавить в корзину, показать цену) в каждом классе. Наследование позволяет описать общее один раз, а в подклассах добавить только специфическое.

    На основе предыдущих уроков

  • «Классы» — синтаксис class, constructor, методы экземпляра
  • «Прототипы» — как методы хранятся в prototype-цепочке
  • «this» — почему this важен в конструкторе
  • extends и super

    class Product {
      constructor(name, price) {
        this.name = name
        this.price = price
      }
    
      getInfo() {
        return `${this.name} — ${this.price} ₽`
      }
    }
    
    class Book extends Product {
      constructor(name, price, author) {
        super(name, price)   // ОБЯЗАТЕЛЬНО первым! Иначе ReferenceError
        this.author = author
      }
    
      getInfo() {
        return super.getInfo() + ` (автор: ${this.author})`
      }
    }
    
    const book = new Book('Чистый код', 1200, 'Роберт Мартин')
    console.log(book.getInfo())
    // 'Чистый код — 1200 ₽ (автор: Роберт Мартин)'

    Правило: в конструкторе дочернего класса super() должен вызываться до любого обращения к this. Нарушение — ReferenceError.

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

    Дочерний класс может полностью заменить метод родителя или дополнить его:

    class Electronics extends Product {
      constructor(name, price, warranty) {
        super(name, price)
        this.warranty = warranty  // гарантия в месяцах
      }
    
      // Полное переопределение
      getInfo() {
        return `${this.name} — ${this.price} ₽, гарантия ${this.warranty} мес.`
      }
    
      // Новый метод только для Electronics
      isUnderWarranty(monthsOld) {
        return monthsOld < this.warranty
      }
    }

    Полиморфизм

    Разные объекты реагируют по-своему на один и тот же вызов метода. Это позволяет писать универсальный код:

    const cart = [
      new Book('JavaScript', 990, 'Кайл Симпсон'),
      new Electronics('MacBook Pro', 180000, 24),
      new Product('Карандаш', 50),
    ]
    
    // Один и тот же метод — разный результат у каждого объекта
    cart.forEach(item => console.log(item.getInfo()))
    // 'JavaScript — 990 ₽ (автор: Кайл Симпсон)'
    // 'MacBook Pro — 180000 ₽, гарантия 24 мес.'
    // 'Карандаш — 50 ₽'

    instanceof — проверка типа

    const laptop = new Electronics('HP Laptop', 60000, 12)
    
    console.log(laptop instanceof Electronics)  // true
    console.log(laptop instanceof Product)      // true  — Electronics наследует Product
    console.log(laptop instanceof Book)         // false

    Типичные ошибки

    1. Забыли вызвать super() в конструкторе:

    // Сломано:
    class Book extends Product {
      constructor(name, price, author) {
        this.author = author  // ReferenceError: Must call super before accessing 'this'
        super(name, price)
      }
    }
    
    // Исправлено:
    class Book extends Product {
      constructor(name, price, author) {
        super(name, price)   // сначала super
        this.author = author
      }
    }

    2. Не вернули расширение из super.method():

    // Сломано — потеряли строку родителя:
    class Book extends Product {
      getInfo() {
        super.getInfo()  // результат проигнорирован!
        return `автор: ${this.author}`
      }
    }
    
    // Исправлено:
    class Book extends Product {
      getInfo() {
        return super.getInfo() + ` (автор: ${this.author})`
      }
    }

    3. Слишком глубокая иерархия:

    // Запах кода — 4+ уровня наследования трудно отлаживать
    class A extends B {}
    class C extends A {}
    class D extends C {}   // уже сложно понять откуда что пришло

    Если иерархия глубже 2-3 уровней — рассмотри композицию вместо наследования.

    В реальных проектах

  • React: class MyComponent extends React.Component — все классовые компоненты наследуют базовый класс React
  • Node.js: class CustomStream extends Readable — расширение встроенных потоков
  • ORM (Prisma, TypeORM): модели наследуют базовый класс с методами save/delete
  • Кастомные ошибки: class NotFoundError extends Error
  • Примеры

    Иерархия товаров интернет-магазина: Product, Book, Electronics

    class Product {
      constructor(name, price, stock = 0) {
        this.name = name
        this.price = price
        this.stock = stock
      }
    
      getInfo() {
        return `${this.name} — ${this.price.toLocaleString('ru-RU')} ₽`
      }
    
      isAvailable() {
        return this.stock > 0
      }
    
      toString() {
        return `[${this.constructor.name}: ${this.name}]`
      }
    }
    
    class Book extends Product {
      constructor(name, price, author, pages, stock) {
        super(name, price, stock)
        this.author = author
        this.pages = pages
      }
    
      getInfo() {
        return super.getInfo() + ` | ${this.author}, ${this.pages} стр.`
      }
    }
    
    class Electronics extends Product {
      constructor(name, price, brand, warranty, stock) {
        super(name, price, stock)
        this.brand = brand
        this.warranty = warranty
      }
    
      getInfo() {
        return super.getInfo() + ` | ${this.brand}, гарантия ${this.warranty} мес.`
      }
    
      isUnderWarranty(monthsOld) {
        return monthsOld <= this.warranty
      }
    }
    
    // Полиморфизм: один код для разных типов
    const catalog = [
      new Book('Чистый код', 1200, 'Роберт Мартин', 431, 5),
      new Book('JavaScript: Хорошие части', 890, 'Дуглас Крокфорд', 176, 0),
      new Electronics('iPhone 15', 89990, 'Apple', 12, 10),
      new Electronics('AirPods Pro', 24990, 'Apple', 6, 3),
    ]
    
    console.log('=== Каталог товаров ===')
    catalog.forEach(item => {
      const status = item.isAvailable() ? 'в наличии' : 'нет в наличии'
      console.log(item.getInfo() + ` [${status}]`)
    })
    // 'Чистый код — 1 200 ₽ | Роберт Мартин, 431 стр. [в наличии]'
    // 'JavaScript: Хорошие части — 890 ₽ | Дуглас Крокфорд, 176 стр. [нет в наличии]'
    // 'iPhone 15 — 89 990 ₽ | Apple, гарантия 12 мес. [в наличии]'
    // 'AirPods Pro — 24 990 ₽ | Apple, гарантия 6 мес. [в наличии]'
    
    // instanceof — проверяем типы
    const phone = catalog[2]
    console.log(phone instanceof Electronics)  // true
    console.log(phone instanceof Product)      // true
    console.log(phone instanceof Book)         // false
    
    // Метод только у Electronics
    if (phone instanceof Electronics) {
      console.log('Гарантия действует 3 месяца?', phone.isUnderWarranty(3))  // true
    }

    Задание

    Ты разрабатываешь систему управления транспортом для сервиса аренды. Создай класс `Vehicle(make, model, year)` с методом `getInfo()`, возвращающим строку вида `"2020 Toyota Camry"`. Создай `Car extends Vehicle` с полем `doors` и override `getInfo()` — добавляет `", 4 дв."` к базовой строке. Создай `Truck extends Vehicle` с полем `payload` (грузоподъёмность в тоннах) и override `getInfo()` — добавляет `", грузоподъёмность: Xт"`. Все три класса должны правильно работать с `instanceof Vehicle`.

    Подсказка

    В getInfo() дочерних классов вызови super.getInfo() чтобы получить базовую строку, затем добавь к ней специфику: return super.getInfo() + ", 4 дв."

    Загружаем среду выполнения...
    Загружаем AI-помощника...