Наследование
Одни классы могут наследоваться от других. Наследование позволяет сократить объем кода в классах-наследниках. Например, возьмем следующие классы:
class Person{
name;
age;
print(){
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee{
name;
age;
company;
print(){
console.log(`Name: ${this.name} Age: ${this.age}`);
}
work(){
console.log(`${this.name} works in ${this.company}`);
}
}
const tom = new Person();
tom.name = "Tom";
tom.age= 34;
const bob = new Employee();
bob.name = "Bob";
bob.age = 36;
bob.company = "Google";
// Name: Tom Age: 34
tom.print();
// Name: Bob Age: 36
bob.print();
// Bob works in Google
bob.work();
Здесь определены два класса - Person
, который представляет человека и Employee
, который представляет работника предприятия. Оба класса прекрасно работают, мы можем создавать их объекты, но мы также видим что класс Employee
повторяет функционал класса Person
, так как работник также является человеком для которого также можно определить свойства name
, age
и метод print
.
Наследование позволяет одним классам автоматически получить функцонал других классов и тем самым сократить объем кода. Для наследования одного класса от другого применяется ключевое слово extends
:
class Base{}
class Derived extends Base{}
После названия класса-наследника ставится ключевое слово extends
, после которого идет имя класса, от которого мы хотим унаследовать функционал. Так, изменим классы Person
и Employee
, применив наследование:
class Person{
name;
age;
print(){
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee extends Person{
company;
work(){
console.log(`${this.name} works in ${this.company}`);
}
}
const tom = new Person();
tom.name = "Tom";
tom.age= 34;
const bob = new Employee();
bob.name = "Bob";
bob.age = 36;
bob.company = "Google";
// Name: Tom Age: 34
tom.print();
// Name: Bob Age: 36
bob.print();
// Bob works in Google
bob.work();
Теперь класс Employee
наследуется от класса Person
. В этом отношении класс Person
еще называется базовым или родительским классом, а Employee
- производным классом или классом-наследником. Поскольку класс Employee
наследует функционал от Person
, то нам нет необходимости заново определять в нем свойства name
, age
и метод print
. В итоге код класса Employee
получился короче, а результат программы тот же.
Наследование класса с конструктором
Вместе со всем функционалом производный класс наследует и конструктор базового класса. Например, определим в базовом классе Person
конструктор:
class Person{
constructor(name, age){
this.name = name;
this.age = age;
}
print(){
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee extends Person{
company;
work(){
console.log(`${this.name} works in ${this.company}`);
}
}
const tom = new Person("Tom", 34);
// Name: Tom Age: 34
tom.print();
// унаследованный конструктор
const sam = new Employee("Sam", 25);
// Name: Sam Age: 25
sam.print();
В данном случае класс Person
определяет конструктор с двумя параметрами. В этом случае класс Employee
наследует его и использует для создания объекта Employee
.
Определение конструктора в классе-наследнике и ключевое слово super
Производный класс также может определить свой конструктор. Если производный класс определяет конструктор, то в нем должен быть вызван конструктор базового класса. Для обращения производном классе к функциональности базового класса, в том числе для обращения к конструктору базового класса, применяется ключевое слово super
.
class Person{
constructor(name, age){
this.name = name;
this.age = age;
}
print(){
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee extends Person{
constructor(name, age, company){
super(name, age);
this.company = company;
}
work(){
console.log(`${this.name} works in ${this.company}`);
}
}
const tom = new Person("Tom", 34);
// Name: Tom Age: 34
tom.print();
const sam = new Employee("Sam", 25, "Google");
// Name: Sam Age: 25
sam.print();
// Sam works in Google
sam.work();
Класс Employee
определяет свой конструктор с тремя параметрами, первой строкой в котором идет обращение к конструктору базового класса Person
:
super(name, age);
Поскольку конструктор класса Person
имеет два параметра, соответственно в него передаются два значения. При этом конструктор базового класса должен вызываться до обращения к свойствам текущего объекта через this
.
Переопределение методов базового класса
Производный класс, как и в случае с конструктором, может переопределять методы базового класса. Так, в примере выше метод print()
класса Person
выводит имя и возраст человека. Но что, если мы хотим, чтобы для работника метод print()
выводил также и компанию? В этом случае мы можем определить в классе Employee
свой метод print()
:
class Person{
constructor(name, age){
this.name = name;
this.age = age;
}
print(){
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee extends Person{
constructor(name, age, company){
super(name, age);
this.company = company;
}
print(){
console.log(`Name: ${this.name} Age: ${this.age}`);
console.log(`Company: ${this.company}`);
}
work(){
console.log(`${this.name} works in ${this.company}`);
}
}
const sam = new Employee("Sam", 25, "Google");
// Name: Sam Age: 25 Company: Google
sam.print();
Однако в коде выше мы видим, что первая строка метода print()
в классе Employee
по сути повторяет код метода print()
из класса Person
. В данном случае это всего одна строка, но в другой ситуации повторяемый код мог бы больше. И чтобы не повторяться, мы опять же можем просто обратиться к реализации метода print()
родительского класса Person
через super
:
class Person{
constructor(name, age){
this.name = name;
this.age = age;
}
print(){
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee extends Person{
constructor(name, age, company){
super(name, age);
this.company = company;
}
print(){
super.print();
console.log(`Company: ${this.company}`);
}
work(){
console.log(`${this.name} works in ${this.company}`);
}
}
const sam = new Employee("Sam", 25, "Google");
// Name: Sam Age: 25 Company: Google
sam.print();
То есть в данном случае вызов представляет вызов реализации метода из базового класса. Таким образом, с помощью this и super мы можем разграничить обращение к функциональности текущего класса или его базового класса:
super.print();
Наследование и приватные поля и методы
При наследовании стоит учитывать, что производный класс может обращаться к любой функциональности базового класса, кроме приватных полей и методов:
class Person{
#name;
constructor(name, age){
this.#name = name;
this.age = age;
}
print(){
console.log(`Name: ${this.#name} Age: ${this.age}`);
}
}
class Employee extends Person{
constructor(name, age, company){
super(name, age);
this.company = company;
}
print(){
super.print();
console.log(`Company: ${this.company}`);
}
work(){
// Ошибка - поле #name недоступно из Employee
console.log(`${this.#name} works in ${this.company}`);
}
}
В данном случае поле #name
в классе Person
определено как приватное, поэтому достуно только внутри этого класса. Поытка обратиться к этому полю в классе-наследнике Employee
приведет к ошибке вне зависимости будет идти обращение через this.#name
или super.#name
.