Наследование классов в JavaScript
Наследование классов в JavaScript: extends и super, цепочка прототипов, переопределение методов, расширение встроенных классов и статические методы.
Введение в наследование классов JavaScript
Наследование классов — фундаментальная концепция объектно-ориентированного программирования, позволяющая одному классу унаследовать свойства и методы другого класса. В JavaScript наследование классов реализуется с помощью ключевого слова extends, которое позволяет создать производный класс, наследующий от базового.
Подробнее о базовом синтаксисе можно узнать в разделе JavaScript: классы и базовый синтаксис.
В этой главе рассматривается создание производных классов, переопределение методов и свойств, вызов родительского кода с помощью super, расширение встроенных классов, таких как Array, наследование статических методов, а также цепочка прототипов, лежащая в основе всего этого механизма.
Как работает наследование: цепочка прототипов
Ключевое слово class является синтаксическим сахаром над прототипным наследованием JavaScript. Когда вы пишете class Circle extends Shape, движок устанавливает два связующих звена:
Circle.prototype.__proto__ === Shape.prototype— методы экземпляра разрешаются по цепочке.Circle.__proto__ === Shape— статические методы также наследуются.
Когда вы обращаетесь к свойству или вызываете метод на объекте, движок сначала ищет его в самом объекте. Если оно не найдено, он поднимается по цепочке прототипов — к Circle.prototype, затем Shape.prototype, затем Object.prototype, затем null — останавливаясь на первом совпадении. Именно поэтому экземпляр Circle может вызвать метод, определённый в Shape.
Object.getPrototypeOf(obj) возвращает следующее звено в цепочке (стандартный и предпочтительный способ её инспектировать). Устаревший аксессор obj.__proto__ указывает на тот же объект. Понимание этой цепочки объясняет всё остальное: переопределение работает потому, что ближайший прототип перекрывает более дальний, а super работает потому, что явно перескакивает к прототипу родителя.
Создание производного класса
Чтобы создать класс, который наследует от другого, используется ключевое слово extends:
В этом примере Circle расширяет Shape, то есть наследует свойства и методы Shape, одновременно предоставляя собственные методы. Обратите внимание, что Circle не определяет собственный конструктор. В JavaScript производные классы без явного конструктора автоматически вызывают super() с теми же аргументами, что были переданы конструктору производного класса.
Переопределение методов
Производные классы могут переопределять методы базовых классов, чтобы обеспечить поведение, специфичное для подкласса.
Здесь Circle переопределяет метод print, чтобы отразить свой конкретный тип.
Вызов родительских методов через super
Из производного класса также можно вызвать метод родительского класса с помощью super.methodName(). Это удобно, когда нужно расширить поведение родителя, а не полностью его заменить.
Здесь super.print() выполняет логику родителя перед добавлением вывода, специфичного для подкласса.
Практический пример переопределения: вычисление площади
Переопределение наиболее полезно, когда каждый подкласс требует принципиально разного поведения. В этом примере базовый класс Shape определяет общий интерфейс, а каждый подкласс переопределяет area() со своим собственным вычислением. Вызов area() на Circle разрешается к Circle.prototype.area, которая перекрывает базовую версию.
Обратите внимание, что describe() определён только в Shape, однако вызывает this.area() и получает реализацию подкласса. Это и есть цепочка прототипов в действии: this всегда указывает на фактический экземпляр, поэтому поиск метода начинается с Circle или Rectangle.
Переопределение свойств и чтение super.prop
super не ограничен только методами — вы можете прочитать свойство, определённое на прототипе родителя, с помощью super.prop. Это удобно, когда подкласс хочет дополнить геттер родителя, а не заменить его.
Доступ к родительскому конструктору: ключевое слово super
Когда класс расширяет другой класс, конструктор производного класса должен вызвать конструктор родителя через super() до того, как сможет использовать this. Вот как super применяется в конструкторах для инициализации родительского класса:
Почему super() должен выполниться до this
В производном классе объект экземпляра не создаётся до выполнения super() — это задача конструктора родителя. До этого момента this находится в неинициализированном состоянии, поэтому любое обращение к нему (чтение, присваивание или даже неявный возврат объекта) вызывает ReferenceError. Это языковое правило, а не рекомендация по стилю.
Связанное правило: если производный класс определяет конструктор, он обязан вызвать super() до его завершения, иначе будет выброшен тот же ReferenceError. (Производный класс без явного конструктора не требует этого — JavaScript автоматически вставляет constructor(...args) { super(...args); }.) После возврата из super() this полностью инициализирован и готов к использованию.
Наследование статических методов
Статические члены принадлежат самому классу, а не его экземплярам. Поскольку extends также связывает Circle.__proto__ с Shape, производные классы наследуют статические методы и могут вызывать их напрямую. Подробнее об их объявлении см. в разделе Статические свойства и методы JavaScript.
Внутри статического метода this ссылается на класс, на котором был вызван метод, поэтому Shape.create, вызванный как Circle.create, создаёт экземпляр Circle.
Многоуровневое наследование
Цепочки могут быть длиннее двух уровней. Поиск метода просто поднимается выше по цепочке прототипов, а super всегда указывает на прототип на один уровень выше класса, в котором определён метод.
Расширение встроенных классов
Вы можете расширять встроенные классы, такие как Array, Error или Map, создавая специализированные версии, которые сохраняют всё встроенное поведение и добавляют собственное.
Особенность Symbol.species. Методы вроде map, filter и slice возвращают новую коллекцию. По умолчанию они возвращают экземпляр вашего подкласса (MyArray), а не обычный Array — что обычно нормально, но может удивить код, ожидающий настоящий Array. Вы можете вернуться к обычным массивам, переопределив статический геттер Symbol.species.
О комбинировании поведения из нескольких источников (в JavaScript нет множественного наследования) см. Примеси в JavaScript.
Резюме
Ключевые выводы:
- Используйте
extendsдля создания производного класса; это связывает как цепочку прототипов (методы экземпляра), так и сам класс (статические методы) с родителем. - Поиск метода и свойства поднимается по цепочке прототипов, останавливаясь на первом совпадении — именно поэтому ближайшее определение переопределяет более дальнее. Инспектируйте цепочку с помощью
Object.getPrototypeOf(). - В конструкторе производного класса вызывайте
super()до обращения кthis. До выполненияsuper()экземпляр не инициализирован, и любое обращение кthisвыбрасываетReferenceError. - Явно обращайтесь к поведению родителя через
super.method()илиsuper.prop, даже если вы его переопределили. - Вы можете расширять встроенные классы, например
Array— только учитывайте поведениеSymbol.species, когда методы возвращают новые коллекции.
Следующие шаги:
- Прототипное наследование JavaScript — механизм, на котором строятся классы.
- Статические свойства и методы JavaScript — объявление и наследование статических членов.
- Примеси в JavaScript — повторное использование поведения между несвязанными классами без единой цепочки наследования.