Объектно-ориентированный JavaScript: наследование
Представьте, вы получили заказ на создание небольшой онлайн игры. Всё что от вас требуется — создать понятный API для работы с персонажами. Техническое задание выглядит следующим образом:
Всего в игре три класса героев: люди, орки и эльфы. Каждый герой, независимо от его принадлежности к какому-либо классу, имеет свой запас здоровья, опыта и силы удара, а также может ходить и бегать. У каждого класса есть своя особенность: люди умеют строить небольшие сооружения для своей защиты, орки, увидев красный цвет, впадают в ярость и становятся в несколько раз сильнее, эльфы же умеют стрелять из лука и способны поразить врага на расстоянии.
Основа
Для создания подобной системы персонажей вам потребуется, как минимум, три функции конструктора: Human
, Orc
и Elf
. Но, если вы внимательно прочитали присланное заказчиком задание, то уже знаете, что любой персонаж обладает одинаковым набором свойств и методов и лишь дополняется каким-то отдельным качеством в зависимости от принадлежности к определённому классу.
Именно в таких случаях и удобно воспользоваться преимуществами наследования. Поэтому, чтобы не повторять один и тот же код для каждого конструктора, можно создать ещё один класс персонажей, от которого и будет происходить наследование всех остальных:
Теперь мы можем создавать базовую заготовку для любого персонажа, используя модуль Character
и конструктор Character
:
Заготовка для всех персонажей создана, и мы можем приступать к созданию отдельных классов. Начнём с людей и модуля Human
. Мы знаем, что любой персонаж, принадлежащий к классу людей, умеет строить сооружения для защиты. Для этого отлично подойдёт метод build
:
Человек, созданный с помощью конструктора Human
теперь умеет строить здания определённой прочности и тем самым увеличивать свой запас здоровья. Отлично, мы уже на полпути! Или нет? Свойства health
у человека пока что нет, поэтому и вся наша конструкция бесполезна. Разумеется, мы бы могли вручную создать все необходимые свойства для каждого класса персонажей:
Но, в таком случае, нам придется дублировать код с присваиванием свойств в каждом конструкторе, поддержка кода заметно усложнится, если, например, у нас будет не 3, а 20 классов героев. Именно для упрощения поддержки и уменьшения количества кода мы и создали конструктор-заготовку. Всё, что нам остаётся сделать — вызвать конструктор Character
внутри конструктора Human
:
Теперь любой объект, созданный с помощью конструктора Human
, обладает свойствами health
, name
, exp
и strength
. В этом легко убедиться:
Разумеется, мы можем использовать и созданный нами ранее метод build
:
apply и call
Методы run
и walk
, которые мы хотели унаследовать от конструктора Character
всё ещё не доступны для использования. Чтобы разобраться, почему именно так, нужно понять принцип работы метода функций apply
.
Итак, у нас есть конструктор Character
, который при вызове с оператором new
выдаст нам полностью рабочий объект со всеми свойствами и методами, как это было показано выше. Но не стоит забывать, что Character
также является обычной функцией, то есть мы можем вызвать её и без использования оператора new
:
Что произойдёт в таком случае? Если вы подобным образом используете функцию в глобальной области видимости, то глобальному объекту window
будут записаны 4 свойства:
Другими словами, вы просто создадите глобальные переменные. Функция Character
использует this
для обращения к текущему объекту. А в глобальной области видимости this
будет ссылаться на объект window
. Таким образом, всё, что делает функция Character
, — присваивает значения объекту, на который ссылается this
при вызове функции. Именно это нам и нужно.
Неплохо было бы вызвать функцию Character
в конструкторе Human
, чтобы быстро записать все свойства в текущий объект. Если вы попробуете вызвать Character
напрямую, то с удивлением обнаружите, что ни одно свойство не было записано в объект:
Вместо этого все четыре свойства опять были записаны в объект window
. Оказалось, что this
не такой умный, как мы ожидали. Но, разумеется, мы можем исправить подобную ситуацию и самостоятельно задать контекст выполнения любой функции с помощью методов call
или apply
. Оба метода первым аргументом принимают значение, которое будет использовано функцией в качестве this
. Далее в метод call
можно передать список аргументов, с которым будет вызвана функция:
Метод apply
работает схожим образом, но вместо списка аргументов принимаем массив (или любую другую массивоподобную структуру) и формирует из него тот же список аргументов. Подобное поведение бывает полезным, когда мы хотим передать вызываемой функции все аргументы из текущей. Сделать это можно с помощью псевдомассива аргументов arguments
:
В нашем случае предпочтительней использовать метод apply
, чтобы передавать в функцию Character
все аргументы, а не только объект настроек. Ведь, возможно, в будущем мы захотим доработать обе функции и добавить несколько новых аргументов.
Чтобы окончательно понять принцип работы метода apply
, попробуте использовать его с любой функцией, принимающей неограниченное число аргументов. Например, функция Math.max
, которая находит максимальное переданное ей число:
С помощью apply
функцию можно заставить работать с массивами:
Так как функция не использует this
, то первым параметром передать можно всё, что угодно:
Наследование
Как я уже писал выше, мы хотим сделать так, чтобы конструктор Human
не только имел все те же свойства, что и Character
, но и мог использовать все методы из его прототипа: walk
и run
. Подобное наследование осуществить очень просто: всё, что нужно сделать — переназначить прототип конструктора Human
:
Метод Object.create
создаёт новый объект с указанным объектом прототипа. Таким образом мы можем использовать методы конструктора Human
, когда они доступны, а в случае, если их нет, то будем обращаться уже к методам конструктора Character
. Подробнее о том, как происходит определение того, какое именно свойство или метод будет использован, можно прочитать в статье о прототипах.
Таким образом для реализации наследования достаточно всего двух строчек кода:
Ещё больше наследования
Мы уже написали конструктор Human
, осталось закончить работу и создать конструкторы для орков и эльфов.
Итого
- Используйте метод
apply
для вызова конструктора родителя внутри потомка для записи свойств. - Для наследования всех методов из прототипа родителя используйте
Object.create
- При наследовании не забывайте явно указывать свойство
constructor
Комментарии