Menu
Русский

Наследование классов в JavaScript: extends и super

Разбираемся, как работает наследование в классах JavaScript: ключевое слово extends, вызов super, переопределение методов и когда вместо наследования лучше взять композицию.

Один класс на основе другого

Наследование в JavaScript позволяет одному классу взять за основу другой и расширить его. Вы бесплатно получаете все поля и методы родителя, а затем добавляете или меняете то, что нужно:

index.js
Output
Click Run to see the output here.

Dog extends Animal означает: «Собака — это Животное, только чуть богаче». У rex нет собственного метода speak, но поиск по цепочке доходит до Animal, где этот метод есть. В этом и весь фокус — обычное прототипное наследование, просто в более удобной обёртке.

super в конструкторе подкласса

Если у подкласса есть свой конструктор, действует одно жёсткое правило: вызвать super(...) нужно раньше, чем вы обратитесь к this. Именно super запускает конструктор родителя, а он, в свою очередь, и создаёт объект, и инициализирует его:

index.js
Output
Click Run to see the output here.

Пропустите строку super(name) — и получите ReferenceError в тот самый момент, когда попытаетесь прочитать или записать this. Движок просто откажется выдавать вам this, пока не отработает родитель.

Если подкласс не объявляет собственный конструктор, JavaScript сгенерирует его сам и прокинет все аргументы в super. Так что писать конструктор вручную нужно только тогда, когда вы добавляете новые поля или делаете какую-то дополнительную инициализацию.

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

Подкласс может переопределить любой унаследованный метод. Побеждает тот, что ближе всех по цепочке прототипов:

index.js
Output
Click Run to see the output here.

Никакой магии тут нет: когда вы вызываете speak() на экземпляре Dog, движок сначала ищет speak на самом объекте, затем на Dog.prototype, находит — и останавливается. До Animal.prototype дело просто не доходит.

Дополняем, а не заменяем: super.method()

Иногда переопределённый метод не должен полностью подменять родительский — хочется дополнить его логику. Именно для этого нужен super.method(...): он вызывает родительскую версию изнутри переопределения:

index.js
Output
Click Run to see the output here.

Вот ради чего наследование и существует: подкласс переиспользует логику родителя, а не копирует её. Если позже мы поменяем Animal.describe, то Dog.describe подхватит это изменение автоматически.

super работает в любом методе, а не только в конструкторе. Он всегда ссылается на родительскую версию того, что вы вызываете.

instanceof и цепочка прототипов

Оператор instanceof проверяет, входит ли указанный класс в цепочку прототипов объекта. Любой экземпляр подкласса одновременно является и экземпляром родительских классов:

index.js
Output
Click Run to see the output here.

Все четыре проверки вернут true. Цепочка выглядит так: Puppy -> Dog -> Animal -> Object, и instanceof просто идёт по ней вверх. Штука полезная для проверки типов, хотя на практике вы будете использовать её реже, чем кажется, — чаще всего код просто вызывает методы, а дальше всё решает полиморфизм.

Пример посерьёзнее

Типичный сценарий: базовый класс с общей логикой и пара наследников, которые её уточняют под свои нужды.

index.js
Output
Click Run to see the output here.

Обрати внимание: метод describe живёт в классе Shape, и его не нужно переписывать в наследниках — он просто вызывает this.area(), а JavaScript в рантайме сам подставит реализацию нужного подкласса. Это и есть полиморфизм: одна и та же точка вызова ведёт себя по-разному в зависимости от того, какой объект перед нами.

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

Наследование подкупает своей продуктивностью — одна строчка, и вам достаётся целый ворох методов. Но со временем, когда иерархия разрастается, всё это начинает трещать по швам.

Простое правило: используйте extends, когда отношение между классами — это честное «X является Y», и подкласс действительно наследует бо́льшую часть поведения родителя. А вот если вам хочется подключить наследование только ради пары вспомогательных методов — лучше взять композицию: положите объект-помощник в поле класса и обращайтесь к нему:

index.js
Output
Click Run to see the output here.

Глубокие цепочки наследования (Animal -> Mammal -> Dog -> WorkingDog -> PoliceDog) отлично смотрятся на диаграммах, но в реальном коде превращаются в головную боль: малейшее изменение ближе к корню непредсказуемо расходится по всем потомкам. Поэтому в живых проектах обычно ограничиваются одним-двумя уровнями иерархии, а остальное решают через композицию.

Что дальше: статические члены класса

Всё, о чём мы говорили в этой статье, живёт на экземплярах — это методы, которые вы вызываете через new Thing().something(). Но иногда методы или данные должны принадлежать самому классу, а не конкретному экземпляру. Именно для этого и нужен static — о нём и поговорим дальше.

Часто задаваемые вопросы

Как работает наследование в JavaScript?

Один класс наследуется от другого через extends. Подкласс получает все методы и поля родителя, может добавлять свои или переопределять существующие. Под капотом JavaScript связывает прототип подкласса с прототипом родителя, поэтому если метод не найден у потомка — поиск автоматически уходит выше по цепочке прототипов.

Зачем нужен super в JavaScript?

super(...) вызывает конструктор родительского класса — и вызвать его нужно обязательно до того, как вы обратитесь к this в конструкторе подкласса. А super.method(...) вызывает родительскую версию метода — это удобно, когда хочется не полностью заменить поведение, а дополнить его.

Что выбрать: наследование или композицию?

Наследование уместно, когда между классами действительно есть связь «является» (is-a) и подкласс разделяет бóльшую часть поведения родителя. Если же вам просто хочется переиспользовать функциональность — берите композицию, то есть объекты, которые содержат другие объекты. Глубокие иерархии классов стареют плохо: в реальных проектах обычно не уходят дальше одного-двух уровней.

Учитесь программировать с Coddy

НАЧАТЬ