Към съдържанието
wannacode

Наследяване

Проблем

Две копия на един и същ код

Програмата за заплати има клас Employee с име, заплата и метод за повишение. После трябваше и мениджър, който има и бонус. Най-бързо беше да копираш Employee в нов клас Manager и да добавиш бонуса.

Месец по-късно някой въвежда повишение -10% вместо 10%. Затова в Employee добавяш проверка. В Manager я забравяш:

// в Employee
raise(percent) {
if (percent > 0) {
this.#salary += Math.trunc(this.#salary * percent / 100);
}
}
// в Manager: копие отпреди проверката
raise(percent) {
this.#salary += Math.trunc(this.#salary * percent / 100);
}
const ivan = new Employee("Иван", 2000);
const maria = new Manager("Мария", 3000, 500);
ivan.raise(-10);
maria.raise(-10);
console.log(String(ivan));
console.log(String(maria));
Изход
Иван: 2000 €
Мария: 2700 €

Заплатата на Иван е защитена, а на Мария падна с 300 €. Двата класа са еднакви на 90%, но всяка поправка трябва да се прави два пъти.

Идеята

Общото е в родителя

Мениджърът е служител, само с нещо повече. Кажи го на езика: class Manager extends Employee. В Manager пишеш само новото, бонуса:

class Manager extends Employee {
#bonus;
constructor(name, salary, bonus) {
super(name, salary);
this.#bonus = bonus;
}
get bonus() {
return this.#bonus;
}
}
const maria = new Manager("Мария", 3000, 500);
maria.raise(-10);
maria.raise(10);
console.log(String(maria));
console.log(`Бонус: ${maria.bonus} €`);
Изход
Мария: 3300 €
Бонус: 500 €

Manager вече има всичко на Employee: име, заплата, raise() с проверката, toString(). Затова повишението с -10% не мина. Проверката е написана веднъж и важи и за двата класа.

Това се казва наследяване. Employee е родителят (базов клас), а Manager е наследникът. Наследникът взима всичко на родителя и добавя своето.

Проверката дали наследяването има смисъл е изречението „X е Y“. „Мениджърът е служител“ е вярно. „Фактурата е служител“ не е, затова фактурата не наследява Employee, дори да има сума и име.

Пробвай

Какво получава стажантът?

Intern няма нищо свое: тялото е празно, {}.

Какво ще отпечата програмата?

class Intern extends Employee {}
const ana = new Intern("Ана", 1200);
ana.raise(5);
console.log(String(ana));
Отговор

Идеята

Първо родителят

#name и #salary са скрити в Employee. Наследникът ги има, но не ги вижда: за него те са скрити като за всеки друг клас. Как тогава Manager им дава стойност?

Като извика конструктора на родителя:

constructor(name, salary, bonus) {
super(name, salary);
this.#bonus = bonus;
}

super(name, salary) вика конструктора на Employee с тези аргументи. Той подрежда #name и #salary, а после Manager подрежда #bonus. В JavaScript super(...) трябва да се извика, преди конструкторът да пипне this. Ако го забравиш, new Manager(...) спира с ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor.

Обектът е един. В него има и частта на родителя, и частта на наследника, а всяка част се подрежда от своя конструктор.

constructor(name, salary, bonus) {
super(name, salary);
this.#bonus = bonus;
}
const maria = new Manager("Мария", 3000, 500);
console.log(`${maria}, бонус ${maria.bonus} €`);
Стъпка по стъпка, с думи
  1. new създава един обект. В него ще има частта на Employee и частта на Manager.
  2. Започва конструкторът на Manager с трите аргумента.
  3. Първо родителят: super(name, salary) вика конструктора на Employee. Преди него this не може да се пипа.
  4. Конструкторът на Employee подрежда своята част: #name и #salary.
  5. Чак после тялото на Manager подрежда бонуса.
  6. Обектът е един, а всяка част е подредена от своя конструктор.
По-точно, за любопитните
  • Конструкторът се наследява. Затова Intern без свой constructor приема името и заплатата на Employee: JavaScript сам вика родителя с аргументите, които си подал.
  • Щом напишеш свой constructor, super(...) става твоя работа. Полетата на наследника (#bonus) се създават чак след като super(...) свърши.

Пробвай

Кой конструктор е пръв?

Тук наследникът не вика родителя изрично:

Какво ще отпечата програмата?

class Account {
constructor() {
console.log("Сметката е открита");
}
}
class SavingsAccount extends Account {
constructor() {
console.log("Лихва: 3%");
}
}
const savings = new SavingsAccount();
Отговор

Пробвай

Намери грешката

Мария трябва да има заплата 3000 € и бонус 500 €. Програмата обаче печата:

Изход
Мария: 500 €

На кой ред е грешката?

Избери реда с грешката.

Идеята

protected няма: долна черта

В края на годината бонусът на мениджъра влиза в заплатата му. Методът е в Manager, но заплатата е #salary в Employee. Getter-ът само чете, а тук трябва да я промениш. А this.#salary в Manager не е позволено: програмата дори не тръгва, SyntaxError: Private field '#salary' must be declared in an enclosing class.

В Java и C# за това има трето ниво на достъп, protected: виждат го класът и наследниците му, а останалите не. JavaScript го няма. Вместо това полето се пише с долна черта, _salary: договорка „вътрешно, за класа и наследниците му“, която езикът не проверява:

class Employee {
#name;
constructor(name, salary) {
this.#name = name;
this._salary = salary;
}
// ... останалото е както преди, с this._salary
}
class Manager extends Employee {
// ... #bonus и конструкторът
mergeBonus() {
this._salary += this.#bonus;
this.#bonus = 0;
}
}
const maria = new Manager("Мария", 3000, 500);
maria.mergeBonus();
console.log(String(maria));
console.log(`Бонус: ${maria.bonus} €`);
Изход
Мария: 3500 €
Бонус: 0 €

Договорката не е защита: maria._salary = 0 в програмата също минава, без грешка. Затова в # остава всичко, което наследникът не трябва да пипа (#name), а долна черта получава само полето, което наистина трябва да сменя.

По-точно, за любопитните
  • # е истинска защита: полето не се стига отвън и от наследниците. Затова и проверката е преди програмата да тръгне, а не по време на работа.

Пробвай

Кое липсва?

Стажантът е служител. Какво трябва да пише на празното място?

class Intern  Employee {}

Идеята

Един родител и общ прародител

Класът има точно един родител. Не може да наследи едновременно Employee и Student. Затова наследяването е единично.

Веригата може да е дълга. Intern наследява Employee. А кого наследява Employee, щом не пише extends? Всеки клас без родител наследява Object. Това е общият родител на всички класове. Затова и празен клас има методи:

class Ticket {}
const ticket = new Ticket();
console.log(ticket.toString());
console.log(ticket);
Изход
[object Object]
Ticket {}

toString() идва от Object и връща [object Object]. В модул „Класове“ писа свой toString(). Сега знаеш, че така заменяш версията на Object. Повече за това е в следващия урок.

console.log(ticket) не вика toString(), а показва класа и полетата: Ticket {}. Затова в този урок печатаме String(maria): String() вика toString().

По-точно, за любопитните
  • В JavaScript няма клас, който не може да се наследява.
  • class WorkingStudent extends Employee, Student не тръгва: SyntaxError. След extends стои точно един клас.

Упражнение

Наследници

Реши задачите в редактора си. Родителските класове са от модул „Класове“. Ако не са ти под ръка, напиши ги наново: те са кратки.

1. Семейна кола. SimpleCar има скорост и метод accelerate(), който я вдига с 10. Напиши FamilyCar, който наследява SimpleCar, започва с 4 места и има метод addSeat(). Създай семейна кола, ускори я и добави място:

Изход
Скорост: 10 км/ч
Места: 5

2. Студент. Person има име, възраст и introduce(). Напиши Student, който наследява Person и добавя среден успех grade (дробно число). Използвай конструктора на родителя.

Изход
Аз съм Елена, на 19 години.
Успех: 5.5

3. Брейк. DanceMove има име, продължителност в секунди и perform(). Напиши BreakDanceMove със сложност от 1 до 10 и метод showComplexity().

Изход
Изпълнявам moonwalk за 5 секунди
Изпълнявам windmill за 12 секунди
windmill: сложност 8 от 10

4. Електрическа кола. Car има марка, гориво в литри и изминати километри. refuel(liters) долива, а drive(distance) харчи 1 литър на 10 км и добавя километрите (ако горивото не стига, казва го и не тръгва). Напиши ElectricCar:

  • без гориво, с батерия, която започва от 100%;
  • с ефективност: колко км изминава с 1% батерия (за „Тесла“ 5);
  • charge() зарежда батерията до 100%;
  • driveElectric(distance) харчи батерия вместо гориво и също добавя километрите.
Ако километрите са #mileage в Car, наследникът не може да ги промени. Подсказка: картата за долната черта.
Изход
Шкода: 150 км, гориво 35 л
Тесла: 200 км, батерия 60%
Батерията не стига за 400 км
Тесла: 600 км, батерия 20%

Още задачи

  • Работник. Worker наследява Person, добавя длъжност и метод work():
Изход
Аз съм Петър, на 34 години.
Работя като счетоводител
  • Стажант с ментор. Добави в Intern поле с името на ментора и го подай в конструктора, заедно с името и заплатата за родителя. Сега Intern вече има нужда от свой constructor.

Тест

Провери се

1. class Manager extends Employee. Кое е вярно?
2. Полето #name е в Employee. Може ли Manager да напише this.#name = "Мария" в свой метод?
3. Какво прави super(name, salary) в конструктора на Manager?
4. Колко родителя може да има един клас?
5. JavaScript няма protected. Как се пише поле, което наследниците ползват, а останалите не бива да пипат?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·