HTML / CSSJavaScriptNode jsПаттерны проектированияПрактические

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

Три кита объектного ориентирования это: наследование, инкапсуляция и полиморфизм. Без четкого понимания этих вещей программисту тяжело написать хороший объектно-ориентированный код, использовать всю силу этого подхода.

Давайте предствами что нам необходимо создать небольгую онлайн игру. Для этого нам требуется создать понятный API для работы c героями.

Всего в игре три класса героев: люди, орки и эльфы. Каждый герой, независимо от его принадлежности к какому-либо классу, имеет свой запас здоровья, опыта и силы удара, а также может ходить и бегать. У каждого класса есть своя особенность: люди умеют строить небольшие сооружения для своей защиты, орки, увидев красный цвет, впадают в ярость и становятся в несколько раз сильнее, эльфы же умеют стрелять из лука и способны поразить врага на расстоянии.

Для начала давайте создадим 3 функции конструктора Human, Orc и Elf.

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

Именно в таких случаях и удобно воспользоваться преимуществами наследования. Поэтому, чтобы не повторять один и тот же код для каждого конструктора, можно создать ещё один класс персонажей, от которого и будет происходить наследование всех остальных:

создадим модуль Character

1const Character = (function() {
2
3 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 };
9
10 Character.prototype.walk = function(steps) {
11 console.log(this.name + ' walked ' + steps + ' steps');
12 };
13
14 Character.prototype.run = function(steps) {
15 console.log(this.name + ' ran ' + 2 * steps + ' steps');
16 };
17
18 return Character;
19})();

Теперь мы можем создавать базовую заготовку для любого персонажа, используя модуль Character и конструктор Character:

1const Character = new Character({
2 name: 'John',
3 strength: 15,
4 exp: 10
5});
6console.log(Character); // {"name":"John", "health":100, "exp":10, "strength":15}
7Character.walk(10); // John walked 10 steps
8Character.run(40); // John ran 80 steps

Базовый класс для персонажей готов, дававйте наачнем создавать отдельные классы для каждого персонажа Начнём с людей и модуля Human. Мы знаем, что любой персонаж, принадлежащий к классу людей, умеет строить сооружения для защиты. Для этого отлично подойдёт метод build:

1const Human = (function() {
2
3 function Human(settings) {};
4
5 Human.prototype.build = function(buildingStrength) {
6 this.health += buildingStrength;
7 };
8
9 return Human;
10})();

Человек, созданный с помощью конструктора Human теперь умеет строить здания определённой прочности и тем самым увеличивать свой запас здоровья. Отлично, мы уже на полпути! Или нет? Свойства health у человека пока что нет, поэтому и вся наша конструкция бесполезна. Разумеется, мы бы могли вручную создать все необходимые свойства для каждого класса персонажей:

1const Human = (function() {
2
3 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 };
9
10 return Human;
11})();

Но, в таком случае, нам придется дублировать код с присваиванием свойств в каждом конструкторе, поддержка кода заметно усложнится, если, например, у нас будет не 3, а 20 классов героев. Именно для упрощения поддержки и уменьшения количества кода мы и создали базовый конструктор. Всё, что нам остаётся сделать — вызвать конструктор Character внутри конструктора Human:

1const Human = (function() {
2
3 function Human (settings) {
4 Character.apply(this, arguments);
5 };
6
7 Human.prototype.build = function(buildingStrength) {
8 this.health += buildingStrength;
9 };
10
11 return Human;
12})();

Теперь любой объект, созданный с помощью конструктора Human, обладает свойствами health, name, exp и strength. В этом легко убедиться:

1const human = new Human({
2 name: 'Snow',
3 exp: 40,
4 strength: 25
5});
6
7console.log(human); // {"name":"Snow", "health":100, "exp":10, "strength":15}

Также мы можем использовать и созданный нами ранее метод build:

1console.log(human.health); // 100
2human.build(10);
3console.log(human.health); // 110
4human.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: 50
6});

Что произойдёт в таком случае? Если вы подобным образом используете функцию в глобальной области видимости, то глобальному объекту window будут записаны 4 свойства:

1console.log(window.name); // 'John'
2console.log(window.health); // 20
3console.log(window.strength); // 10
4console.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})();
7
8const human = new Human({health: 10});
9
10console.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})();
7
8const human = new Human({name: 'John', health: 10});
9
10console.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})();
7
8const human = new Human({name: 'John', health: 10});
9
10console.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 };
5
6 Human.prototype = Object.create(Character.prototype);
7 Human.prototype.constructor = Human;
8
9 Human.prototype.build = function(buildingStrength) {
10 this.health += buildingStrength;
11 };
12
13 return Human;
14})();
15
16const human = new Human({name: 'John', health: 10});
17
18console.log(human.health); // 10
19human.build(10);
20console.log(human.health); // 20
21human.walk(10); // John walked 10 steps
22human.run(50); // John ran 100 steps

Метод Object.create создаёт новый объект с указанным объектом прототипа. Таким образом мы можем использовать методы конструктора Human, когда они доступны, а в случае, если их нет, то будем обращаться уже к методам конструктора Character. Подробнее о том, как происходит определение того, какое именно свойство или метод будет использован, можно прочитать тут.

Таким образом для реализации наследования достаточно всего двух строчек кода:

1Child.prototype = Object.create(Parent.prototype);
2Child.prototype.constructor = Child;

Создадим конструкторы для орков и эльфов.

1const Orc = (function() {
2
3 function Orc(settings) {
4 Character.apply(this, arguments);
5 };
6
7 Orc.prototype = Object.create(Character.prototype);
8 Orc.prototype.constructor = Orc;
9
10 Orc.prototype.getAngry = function(color, times) {
11 if (color === 'red') {
12 this.damage *= times;
13 }
14 };
15
16 return Orc;
17})();
18
19const Elf = (function() {
20
21 function Elf(settings) {
22 Character.apply(this, arguments);
23 };
24
25 Elf.prototype = Object.create(Character.prototype);
26 Elf.prototype.constructor = Elf;
27
28 Elf.prototype.shoot = function(distance) {
29 console.log(this.name + ' shot an arrow to' + distance + ' meters');
30 };
31
32 return Elf;
33})();
ℹ️Итого
  1. Используйте метод apply для вызова конструктора родителя внутри потомка для записи свойств.
  2. Для наследования всех методов из прототипа родителя используйте Object.create
  3. При наследовании не забывайте явно указывать свойство constructor

Практика

Задание 1

Создайте объект с именем Teacher, производный от класса Person, и реализуйте метод с именем learn, который получает строку с именем subject и распечатывает:

1[teacher's name] сейчас преподает [subject]
1const Person = function() {};
2
3Person.prototype.initialize = function(name, age)
4{
5 this.name = name;
6 this.age = age;
7}
8
9// TODO: создать класс Учителя и метод teach
10const 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();
17
18him.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.

Задание считается выполненным когда все тесты будут подсвечены зеленым ✅

Original source
Hello