Наследование в объектно-ориентированном программировании JavaScript

Источник: «Inheritance In JavaScript Object-Oriented Programming»
Наследование в объектно-ориентированном программировании (ООП) JavaScript — фундаментальная концепция, которая развивалась с течением времени, и в этой статье мы покажем, как она работает и как её использовать.

Мы рассматриваем настоящее наследование классов, а не просто прототипное наследование между экземплярами. Терминология класса здесь используется только для того, чтобы упростить понимание того, что мы собираемся делать. Вот что мы собираемся сделать: мы создадим новый класс student, который наследуется от класса person. Родительским классом будет person, а дочерним student. Это потому, что student по сути является подтипом persona.

student — это person, более конкретный индивидуум, и эти отношения, по сути, представляют собой концепцию наследования, которую мы рассмотрим в этой статье. Для начала воспользуемся функциями конструкторами. Мы будем наследовать между классами, используя функции конструкторы на первом этапе, что потребует некоторой работы, но поможет вам понять, как именно строится цепочка прототипов, чтобы разрешить наследование между свойствами прототипов двух отдельных функций конструкторов. Затем на следующем шаге мы проделаем то же самое, используя классы ES6, что будет намного проще.

Наконец, мы воспользуемся object.create. Давайте приведём это в действие прямо сейчас.

Наследование между классами: Функции конструкторы

Для student строиться функция конструктор. Мы хотим, чтобы дочерний класс имел ту же функциональность, что и родительский класс, но с некоторым дополнительным функционалом. Мы передаём тот же аргумент в дочерний класс, а затем некоторые дополнительные.

const person = function (firstName, birthYear) {
this.firstName = firstName;
this.birthYear = birthYear;
};

person.prototype.calcAge = function () {
console.log(2022 - this.birthYear);
};

const student = function (firstName, birthYear, course) {
this.firstName = firstName;
this.birthYear = birthYear;
this.course = course;
};

Вышеприведённое показывает, что класс student имеет почти те же данные, что и person.

Мы можем создать нового студента.

const paul = new student("Paul", 2010, "Computer Science");
console.log(paul);

Мы также можем добавить метод, изменив свойство прототипа.

//Добавляем метод
student.prototype.introduce = function () {
console.log(`My name is ${this.firstName} and I study ${this.course}`);
};

paul.introduce();

Пока это то, что мы создали. Это не что иное, как функция конструктор student и её атрибут прототип. Кроме того, объект paul связан со своим прототипом, и, конечно же, прототип является атрибутом прототипа функции конструктора. Создавая объект paul с помощью оператора new, эта связь между экземпляром и прототипом теперь создаётся автоматически.

Нам придётся создать это соединение вручную. Чтобы связать эти два объекта-прототипа, мы будем использовать object.create, потому что определение прототипов вручную — это именно то, что делает object.create. student — это person; в результате мы хотим, чтобы student и person были связаны. Мы заинтересованы в том, чтобы класс student был дочерним классом, происходящим от класса person, родительского класса. В результате все вхождения student получат доступ ко всему в свойстве прототипа, например к функции calcAge. В этом весь смысл наследования. Родительские классы могут поделиться своим поведением со своими дочерними классами.

Мы хотим сделать person.prototype прототипом student.prototype или, другими словами, мы хотим установить прото свойство для student.prototype в person.prototype. Мы будем использовать object.create, чтобы связать эти два объекта-прототипа, потому что определение прототипов — это именно то, что делает object.create. При этом объект student.prototype теперь наследуется от person.prototype. Теперь мы должны создать это соединение, прежде чем добавлять дополнительные методы к объекту прототипу student. Это потому, что object.create вернёт пустой объект. На данный момент student.prototype пуст. Затем к этому пустому объекту мы можем добавить методы.

Теперь, когда всё это есть, мы можем вызвать метод calcAge даже в дочернем классе.

const person = function (firstName, birthYear) {
this.firstName = firstName;
this.birthYear = birthYear;
};

person.prototype.calcAge = function () {
console.log(2022 - this.birthYear);
};

const student = function (firstName, birthYear, course) {
person.call(this, firstName, birthYear);
this.course = course;
};

//Linking Prototypes
student.prototype = Object.create(person.prototype);
//Add in a method
student.prototype.introduce = function () {
console.log(`My name is ${this.firstName} and i study ${this.course}`);
};

const paul = new student("Paul", 2010, "Computer Science");
paul.introduce();
paul.calcAge();

Мы уже знаем, что это сработало благодаря цепочке прототипов, но давайте посмотрим, как именно. Когда мы вызываем paul.calcAge(), мы фактически выполняем поиск свойства или метода, JavaScript пытается найти запрошенное свойство или метод. Метод calcAge не находится непосредственно в объекте paul, а также не находится в прототипе paul. Всякий раз, когда мы пытаемся получить доступ к методу не объекта или его прототипа, JavaScript будет искать ещё дальше в цепочке прототипов. Наконец-то JavaScript найдёт calcAge в person.prototype. Именно по этой причине мы создали цепочку прототипов таким образом, чтобы объект paul мог наследовать любые методы из своего родительского класса. Таким образом, теперь мы можем вызывать метод прототипа person на объекте student, и он будет работать. Это сила наследования.

Так что, как и раньше, object.prototype будет находиться на вершине цепочки прототипов. Неважно, как далеко в цепочке прототипов находится метод; теперь у нас есть полная картина того, как наследование между классами работает с конструкторами функций.

Наследование между классами: Классы ES6

Это работает так же внутри с классами ES6; всё, что меняется, это синтаксис.

Давайте скопируем исходный класс person, созданный ранее. Итак, мы можем наследовать от класса.

const steven = Object.create(personProto);
const studentProto = Object.create(personProto);
class personCl {
constructor(fullName, birthYear) {
this.fullName = fullName;
this.birthYear = birthYear;
}

calcAge() {
console.log(2022 - birthYear);
}
}

Синтаксис класса скрывает множество деталей, происходящих за кулисами, потому что классы — это всего лишь слой препятствий для функций конструкторов. Чтобы реализовать наследование между классами ES6, нам нужны только два ингредиента: ключевые слова extends и super.

Одно только ключевое слово extends будет ссылаться на прототипы за кулисами, даже не задумываясь об этом. Тогда, конечно, нам ещё нужен конструктор. Это будет, как раньше, получить те же аргументы, что и родительский класс, плюс некоторые дополнительные, такие как birthYear и course. Нам не нужно вручную вызывать personCI.call, как мы делали это раньше в функции конструкторе. Вместо этого мы вызываем функцию super. Функция super — это функция конструктор родительского класса. Идея по-прежнему похожа на то, что мы сделали в функции конструкторе, но здесь всё происходит автоматически. Нам не нужно снова указывать имя родительского класса, потому что это уже произошло. Всё, что мы делаем, это передаём аргументы конструктору родительского класса.

Вызов функции super всегда должен происходить первым, потому что вызов функции super отвечает за создание ключевого слова this в подклассе. Тем не менее дополнительный параметр и аргумент не являются обязательными.

class studentCl extends personCl {
constructor(fullName, birthYear, course) {
//Always needs to happen first
super(fullName, birthYear);
this.course = course;
}

introduce() {
console.log(`My name is ${this.fullName} and i study ${this.course}`);
}
}

Теперь давайте создадим нового ученика,

const martha = new studentCl ('Martha', 2012, 'computer science')

Теперь мы можем вызвать метод calcAge и метод introduce.

martha.introduce();
martha.calcAge();

Если мы посмотрим на прототип martha, то увидим, что там есть метода introduce и calcAge. Это доказывает, что цепочка прототипов была настроена автоматически с помощью ключевого слова extends.

Наследование между классами: Object.create

Теперь давайте создадим объект, который будет служить прототипом для создания нового объекта person с помощью object.create. Это будет наш родительский класс.

Object.create проще в использовании и понимании, и реализовать аналогичную иерархию между person и student не составит труда.

const personProto = {
calcAge() {
console.log(2022 - this.birthYear);
},

init(firstName, birthYear) {
this.firstName = firstName;
this.birthYear = birthYear;
},
};

personProto является прототипом всех новых объектов person, теперь мы хотим добавить ещё один прототип в средине цепочки. Между personProto и объектом. Мы собираемся заставить student наследовать напрямую от person, и мы создадим объект, который будет прототипом для student.

const studentProto = Object.create(personProto);
//мы можем использовать этот прототип student для создания новых студентов
const ray = Object.create(studentProto);

Объект studentProto, который мы только что создали, теперь является прототипом объекта ray. Опять, объект studentProto теперь является прототипом ray, а объект personProto, в свою очередь, является прототипом studentProto. Следовательно, personProto является родительским прототипом ray, что означает, что он находится в его цепочке прототипов.

Всё начинается с объекта personProto, который раньше был прототипом всех объектов person, но теперь, используя object.create, мы делаем так, чтобы personProto стал прототипом studentProto, и это, по сути, наследует student от person. Мы уже установили отношения между родителями и детьми, которые искали. Чтобы закончить, всё, что нам нужно сделать, это снова использовать object.create, но на этот раз для создания нового фактического объект student. ray наследуется от personProto, который теперь является прототипом ray; поэтому объект ray сможет использовать все методы в studentProto и personProto.

После того как цепочка областей видимости установлена правильно, давайте также добавим метод init в studentProto, который будет получать firstName, birthYear и course точно так же, как мы сделали с personProto, чтобы нам не нужно было вручную указывать свойства для любого нового объекта student.

studentProto.init = function (firstName, birthYear, course) {
personProto.init.call(this, firstName, birthYear);
this.course = course;
};

studentProto.introduce = function () {
console.log(`My name is ${this.firstName} and i study ${this.course}`);
};

const ray = Object.create(studentProto);
ray.init("Ray", 2010, "computer science");
ray.introduce();
ray.calcAge();

Если мы хотим посмотреть на прототип ray, то увидим, что там есть методы init и introduce. Затем внутри внутреннего прототипа можно найти метод calcAge. В этом Object.create мы не беспокоимся о функции конструкторе или свойствах прототипа, это просто объекты, связанные с другими объектами. Это просто и красиво, если вы спросите меня. На самом деле, некоторые люди думают, что этот шаблон намного лучше, чем попытка подделки классов в JavaScript. Потому что подделка классов в том виде, в котором они существуют в других языках, таких как Java или C++, — это именно то, что мы делаем, используя функции конструктора и даже классы ES6. Но здесь, в этой технике, которую я только что показал вам с помощью Object.create, мы на самом деле не подделываем классы.

Мы просто связываем объекты вместе, при этом одни объекты затем служат прототипами других объектов. Я бы не возражал, если бы это был единственный способ реализации ООП в JavaScript, но классы и конструкторы ES6 гораздо чаще используются в реальном мире. Тем не менее по-прежнему очень важно и ценно, чтобы вы изучили эти три техники сейчас, потому что вы увидите их все. Это также позволяет вам подумать об этом и выбрать стиль, который нравиться больше всего.

Заключение

Наследование позволяет создать класс наследующий все функции родительского класса и добавляет новые возможности. Класс может наследовать все методы и атрибуты другого класса через наследование класса. В JavaScript наследование поддерживается с помощью объектов-прототипов, а наследование — полезная функция, позволяющая повторно использовать код.

Надеюсь, эта статья о концепции наследования в JavaScript оказалась для вас полезной!

Дополнительные материалы

Предыдущая Статья

Знакомство с примитивными объектами в JavaScript (первая часть)

Следующая Статья

Руководство по семантическому HTML