Наследование
Три кита объектного ориентирования это: наследование, инкапсуляция и полиморфизм. Без четкого понимания этих вещей программисту тяжело написать хороший объектно-ориентированный код, использовать всю силу этого подхода.
Давайте предствами что нам необходимо создать небольгую онлайн игру. Для этого нам требуется создать понятный API для работы c героями.
Всего в игре три класса героев: люди, орки и эльфы. Каждый герой, независимо от его принадлежности к какому-либо классу, имеет свой запас здоровья, опыта и силы удара, а также может ходить и бегать. У каждого класса есть своя особенность: люди умеют строить небольшие сооружения для своей защиты, орки, увидев красный цвет, впадают в ярость и становятся в несколько раз сильнее, эльфы же умеют стрелять из лука и способны поразить врага на расстоянии.
Для начала давайте создадим 3 функции конструктора Human, Orc и Elf.
Но как м могли заметить, любой персонаж обладает одинаковым набором свойств и методов и лишь дополняется каким-то отдельным качеством в зависимости от принадлежности к определённому классу.
Именно в таких случаях и удобно воспользоваться преимуществами наследования. Поэтому, чтобы не повторять один и тот же код для каждого конструктора, можно создать ещё один класс персонажей, от которого и будет происходить наследование всех остальных:
создадим модуль Character
1const Character = (function() {23 function Character(settings) {4 this.name = settings.name;5 this.health = settings.health || 100;6 this.exp = settings.exp || 0;7 this.strength = settings.strength || 1;8 };910 Character.prototype.walk = function(steps) {11 console.log(this.name + ' walked ' + steps + ' steps');12 };1314 Character.prototype.run = function(steps) {15 console.log(this.name + ' ran ' + 2 * steps + ' steps');16 };1718 return Character;19})();
Теперь мы можем создавать базовую заготовку для любого персонажа, используя модуль Character и конструктор Character:
1const Character = new Character({2 name: 'John',3 strength: 15,4 exp: 105});6console.log(Character); // {"name":"John", "health":100, "exp":10, "strength":15}7Character.walk(10); // John walked 10 steps8Character.run(40); // John ran 80 steps
Базовый класс для персонажей готов, дававйте наачнем создавать отдельные классы для каждого персонажа
Начнём с людей и модуля Human. Мы знаем, что любой персонаж, принадлежащий к классу людей, умеет строить сооружения для защиты. Для этого отлично подойдёт метод build:
1const Human = (function() {23 function Human(settings) {};45 Human.prototype.build = function(buildingStrength) {6 this.health += buildingStrength;7 };89 return Human;10})();
Человек, созданный с помощью конструктора Human теперь умеет строить здания определённой прочности и тем самым увеличивать свой запас здоровья. Отлично, мы уже на полпути! Или нет? Свойства health у человека пока что нет, поэтому и вся наша конструкция бесполезна. Разумеется, мы бы могли вручную создать все необходимые свойства для каждого класса персонажей:
1const Human = (function() {23 function Human (settings) {4 this.name = settings.name;5 this.health = settings.health || 100;6 this.exp = settings.exp || 0;7 this.strength = settings.strength || 1;8 };910 return Human;11})();
Но, в таком случае, нам придется дублировать код с присваиванием свойств в каждом конструкторе, поддержка кода заметно усложнится, если, например, у нас будет не 3, а 20 классов героев. Именно для упрощения поддержки и уменьшения количества кода мы и создали базовый конструктор. Всё, что нам остаётся сделать — вызвать конструктор Character внутри конструктора Human:
1const Human = (function() {23 function Human (settings) {4 Character.apply(this, arguments);5 };67 Human.prototype.build = function(buildingStrength) {8 this.health += buildingStrength;9 };1011 return Human;12})();
Теперь любой объект, созданный с помощью конструктора Human, обладает свойствами health, name, exp и strength. В этом легко убедиться:
1const human = new Human({2 name: 'Snow',3 exp: 40,4 strength: 255});67console.log(human); // {"name":"Snow", "health":100, "exp":10, "strength":15}
Также мы можем использовать и созданный нами ранее метод build:
1console.log(human.health); // 1002human.build(10);3console.log(human.health); // 1104human.build(120);5console.log(human.health); // 230
apply и call
Методы run и walk, которые мы хотели унаследовать от конструктора Character всё ещё не доступны для использования. Чтобы разобраться, почему именно так, нужно понять принцип работы метода функций apply.
Итак, у нас есть конструктор Character, который при вызове с оператором new выдаст нам полностью рабочий объект со всеми свойствами и методами, как это было показано выше. Но не стоит забывать, что Character также является обычной функцией, то есть мы можем вызвать её и без использования оператора new:
1Character({2 name: 'John',3 health: 20,4 strength: 10,5 exp: 506});
Что произойдёт в таком случае? Если вы подобным образом используете функцию в глобальной области видимости, то глобальному объекту window будут записаны 4 свойства:
1console.log(window.name); // 'John'2console.log(window.health); // 203console.log(window.strength); // 104console.log(window.exp); // 50
Другими словами, вы просто создадите глобальные переменные. Функция Character использует this для обращения к текущему объекту. А в глобальной области видимости this будет ссылаться на объект window. Таким образом, всё, что делает функция Character, — присваивает значения объекту, на который ссылается this при вызове функции. Именно это нам и нужно.
Неплохо было бы вызвать функцию Character в конструкторе Human, чтобы быстро записать все свойства в текущий объект. Если вы попробуете вызвать Character напрямую, то с удивлением обнаружите, что ни одно свойство не было записано в объект:
1const Human = (function() {2 function Human (settings) {3 Character(settings);4 };5 return Human;6})();78const human = new Human({health: 10});910console.log(human); // {}
Вместо этого все четыре свойства опять были записаны в объект window. Оказалось, что this не такой умный, как мы ожидали. Но, разумеется, мы можем исправить подобную ситуацию и самостоятельно задать контекст выполнения любой функции с помощью методов call или apply. Оба метода первым аргументом принимают значение, которое будет использовано функцией в качестве this. Далее в метод call можно передать список аргументов, с которым будет вызвана функция:
1const Human = (function() {2 function Human(settings) {3 Character.call(this, settings);4 };5 return Human;6})();78const human = new Human({name: 'John', health: 10});910console.log(human); // {name: "John", health: 10, exp: 0, strength: 1}
Метод apply работает схожим образом, но вместо списка аргументов принимаем массив (или любую другую массивоподобную структуру) и формирует из него тот же список аргументов. Подобное поведение бывает полезным, когда мы хотим передать вызываемой функции все аргументы из текущей. Сделать это можно с помощью псевдомассива аргументов arguments:
1const Human = (function() {2 function Human(settings) {3 Character.apply(this, arguments);4 };5 return Human;6})();78const human = new Human({name: 'John', health: 10});910console.log(human); // {name: "John", health: 10, exp: 0, strength: 1}
В нашем случае предпочтительней использовать метод apply, чтобы передавать в функцию Character все аргументы, а не только объект настроек. Ведь, возможно, в будущем мы захотим доработать обе функции и добавить несколько новых аргументов.
Наследование
Нам нужно сделать так чтобы конструктор Human не только имел все те же свойства, что и Character, но и мог использовать все методы из его прототипа: walk и run. Подобное наследование осуществить очень просто: всё, что нужно сделать — переназначить прототип конструктора Human:
1const Human = (function() {2 function Human(settings) {3 Character.apply(this, arguments);4 };56 Human.prototype = Object.create(Character.prototype);7 Human.prototype.constructor = Human;89 Human.prototype.build = function(buildingStrength) {10 this.health += buildingStrength;11 };1213 return Human;14})();1516const human = new Human({name: 'John', health: 10});1718console.log(human.health); // 1019human.build(10);20console.log(human.health); // 2021human.walk(10); // John walked 10 steps22human.run(50); // John ran 100 steps
Метод Object.create создаёт новый объект с указанным объектом прототипа. Таким образом мы можем использовать методы конструктора Human, когда они доступны, а в случае, если их нет, то будем обращаться уже к методам конструктора Character.
Подробнее о том, как происходит определение того, какое именно свойство или метод будет использован, можно прочитать тут.
Таким образом для реализации наследования достаточно всего двух строчек кода:
1Child.prototype = Object.create(Parent.prototype);2Child.prototype.constructor = Child;
Создадим конструкторы для орков и эльфов.
1const Orc = (function() {23 function Orc(settings) {4 Character.apply(this, arguments);5 };67 Orc.prototype = Object.create(Character.prototype);8 Orc.prototype.constructor = Orc;910 Orc.prototype.getAngry = function(color, times) {11 if (color === 'red') {12 this.damage *= times;13 }14 };1516 return Orc;17})();1819const Elf = (function() {2021 function Elf(settings) {22 Character.apply(this, arguments);23 };2425 Elf.prototype = Object.create(Character.prototype);26 Elf.prototype.constructor = Elf;2728 Elf.prototype.shoot = function(distance) {29 console.log(this.name + ' shot an arrow to' + distance + ' meters');30 };3132 return Elf;33})();
Итого
- Используйте метод apply для вызова конструктора родителя внутри потомка для записи свойств.
- Для наследования всех методов из прототипа родителя используйте Object.create
- При наследовании не забывайте явно указывать свойство constructor
Практика
Задание 1
Создайте объект с именем Teacher, производный от класса Person, и реализуйте метод с именем learn, который получает строку с именем subject и распечатывает:
1[teacher's name] сейчас преподает [subject]
1const Person = function() {};23Person.prototype.initialize = function(name, age)4{5 this.name = name;6 this.age = age;7}89// TODO: создать класс Учителя и метод teach10const Teacher = function() {11 this.teach = function(subject){12 console.log(this.name + " is now teaching " + subject);13 }14}15Teacher.prototype = new Person();16const him = new Teacher();1718him.initialize("Adam", 45);19him.teach("Inheritance");
Задание 2
Создадайте два подкласса Doctor и Nurse из родительского класса с именем HospitalEmployee.
Doctor
Свойства: _name, _remainingVacationDays (в конструкторе установлено значение 20 по умолчанию), _insurance.
Методы: .takeVacationDays()
Nurse
Свойства: _name, _remainingVacationDays (в конструкторе установлено значение 20 по умолчанию), _certifications
Методы: .takeVacationDays(), .addCertification()
создайте родительский класс с именем HospitalEmployee. Добавьте конструктор с именем в качестве аргумента.
Внутри конструктора задайте для _name экземпляра значение name, а для _remainingVacationDays значение 20.
В конструкторе создайте геттеры для ваших свойств _name и _remainingVacationDays.
Добавьте метод takeVacationDays.
Этот метод должен принимать один аргумент с именем daysOff.
Внутри метода вычтите daysOff из _remainingVacationDays. Сохраните результат в _remainingVacationDays.
Задание 3
Классы Car и Truck имеют схожее поведение и свойства. Используя код ниже перепишите их таким образом, чтобы они наследовали повторяющее свойства с базового класса Vehicle.
Задание считается выполненным когда все тесты будут подсвечены зеленым ✅