Наследование прототипов конструкторов

Последнее обновление: 18.11.2023

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

Например, у нас может быть объект Person, который представляет отдельного пользователя. И также может быть объект Employee, который представляет работника. Но работник также может являться пользователем и поэтому должен иметь все его свойства и методы. Например:

// конструктор пользователя
function Person (name, age) {
    this.name = name;
    this.age = age;
    this.sayHello = function(){
        console.log(`Person ${this.name} says "Hello"`);
    };
}
// добавляем прототип в функцию
Person.prototype.print = function(){
    console.log(`Name: ${this.name}  Age: ${this.age}`);
};


// конструктор работника
function Employee(name, age, comp){
    Person.call(this, name, age);         // применяем конструктор Person
    this.company = comp;
    this.work = function(){
        console.log(`${this.name}  works in ${this.company}`);
    };
}
// наследуем прототип от Person
Employee.prototype = Object.create(Person.prototype);
// устанавливаем конструктор 
Employee.prototype.constructor = Employee;

Здесь в начале определяет функция-конструктор Person, который представляет пользователя. В Person определены два свойства и два метода. Для примера один мето - sayHello определен внутри конструктора, а второй метод - print определен непосредственно в прототипе.

Затем определяется функция-конструктор Employee, который представляет работника.

В конструкторе Employee происходит обращение к конструктору Person с помощью вызова:

Person.call(this, name, age);

Передача первого параметра позволяет вызвать функцию конструктора Person для объекта, создаваемого конструктором Employee. Благодаря этому все свойства и методы, определенные в конструкторе Person, также переходят на объект Employee. Дополнительно определяется свойство company, которое представляет компанию работника, и метод work.

Кроме того, необходимо унаследовать также и прототип Person и соответственно все определенные через прототип функции (например, в примере выше это функция Person.prototype.print). Для этого служит вызов:

Employee.prototype = Object.create(Person.prototype);

Метод Object.create() позволяет создать объект прототипа Person, который затем присваивается прототипу Employee.

Нередко вместо вызова метода Object.create() для установки прототипа используется вызов наследуемого конструктора, например:

Employee.prototype = new Person();

В результате будет создан объект, у которого прототип (Employee.prototype.__proto__) будет указывать на прототип Person

Однако стоит учитывать, что созданный объект прототипа будет указывать на конструктор Person. Поэтому также устанавливаем нужный конструктор:

Employee.prototype.constructor = Employee;

Конструктор редко используется сам по себе, и, возможно, осутствие установки конструктора никак не скажется на работе программы. Но тем не менее рассмотрим следующую ситуацию

const obj = new Employee.prototype.constructor("Bob", 23, "Google");
console.log(obj); // Employee или Person в зависимости от типа конструктора
obj.work(); // Если obj -  Person, то будет ошибка

Здесь напрямую вызываем конструктор для создания объекта obj. И тип объекта obj здесь будет зависеть от того, какой конструктор установлен для Employee.prototype.constructor

Протестируем выше определенные функции-конструкторы:

// конструктор пользователя
function Person (name, age) {
    this.name = name;
    this.age = age;
    this.sayHello = function(){
        console.log(`Person ${this.name} says "Hello"`);
    };
}
Person.prototype.print = function(){
    console.log(`Name: ${this.name}  Age: ${this.age}`);
};

// конструктор работника
function Employee(name, age, comp){
    Person.call(this, name, age);         // применяем конструктор Person
    this.company = comp;
    this.work = function(){
        console.log(`${this.name}  works in ${this.company}`);
    };
}
// наследуем прототип от Person
Employee.prototype = Object.create(Person.prototype);

// устанавливаем конструктор 
Employee.prototype.constructor = Employee;

// создаем объект Employee
const tom = new Employee("Tom", 39, "Google");
// обращение к унаследованному свойству
console.log("Age:", tom.age);
// обращение к унаследованному методу
tom.sayHello();    // Person Tom says "Hello"
// обращение к унаследованному методу прототипа
tom.print();    // Name: Tom  Age: 39 
// обращение к собственному методу
tom.work();    // Tom works in Google

Переопределение функций

При наследовании мы можем переопределять наследуемый функционал. Например, в примере выше для Person определено два метода: sayHello (в конструкторе) и print() (в прототипе). Но, допустим, для Employee мы хотим изменить их логику, например, в методе print также выводить компанию работника. В этом случае мы можем определить для Employee методы с теми же именами:

function Person (name, age) {
    this.name = name;
    this.age = age;
    this.sayHello = function(){
        console.log(`Person ${this.name} says "Hello"`);
    };
}
Person.prototype.print = function(){
    console.log(`Name: ${this.name}  Age: ${this.age}`);
};

function Employee(name, age, comp){
    Person.call(this, name, age);
    this.company = comp;
    // переопределяем метод sayHello
    this.sayHello = function(){
        console.log(`Employee ${this.name} says "Hello"`);
    };
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;

// переопределяем метод print
Employee.prototype.print = function(){
    console.log(`Name: ${this.name}  Age: ${this.age}  Company: ${this.company}`);
};

const tom = new Employee("Tom", 39, "Google");
tom.sayHello();    // Employee Tom says "Hello"
tom.print();    // Name: Tom  Age: 39  Company: Google

Метод sayHello() определен внутри конструктора Person, поэтому данный метод переопределяется внутри конструктора Employee. Метод print() определен как метод прототипа Person, поэтому его можно переопределить в прототипе Employee.

Вызов метода родительского прототипа

В прототипе-наследнике может потребоваться вызвать метод из родительского прототипа. Например, это может быть необходимо для сокращении логики кода/, если логика метода наследника повторяет логику метода родителя. В этом случае для обращения к методам родительского прототипа применяется функция call()():

function Person (name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.print = function(){
    console.log(`Name: ${this.name}  Age: ${this.age}`);
};

function Employee(name, age, comp){
    Person.call(this, name, age);
    this.company = comp;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;

// переопределяем метод print
Employee.prototype.print = function(){
    Person.prototype.print.call(this); // вызываем метод print из Person
    console.log(`Company: ${this.company}`);
};

const tom = new Employee("Tom", 39, "Google");
tom.print();    // Name: Tom  Age: 39  
                // Company: Google

В данном случае при переопределении метода print в прототипе Employee вызывается метод print из прототипа Person:

Employee.prototype.print = function(){
    Person.prototype.print.call(this); // вызываем метод print из Person
    console.log(`Company: ${this.company}`);
};

Проблемы прототипного наследования

Стоит отметить, что тип Employee перенимает не только все текущие свойства и методы из прототипа Person, но и также те, которые будут впоследствии добавляться динамически. Например:

const tom = new Employee("Tom", 39, "Google");
Person.prototype.sleep = function() {console.log(`${this.name} sleeps`);}
tom.sleep();

Здесь в прототип Person добавляется метод sleep. Причем она добавляется уже после создания объекта tom, который представляет тип Employee. Тем не менее даже у этого объекта мы можем вызвать метод sleep.

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

function Person (name, age) {
    this.name = name;
    this.age = age;
    this.sayHello = function(){
        console.log(`Person ${this.name} says "Hello"`);
    };
}
Person.prototype.print = function(){
    console.log(`Name: ${this.name}  Age: ${this.age}`);
};

function Employee(name, age, comp){
    Person.call(this, name, age);
    this.company = comp;
}
// наследуем прототип от Person
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;

// меняем метод print в базовом прототипе Person
Employee.prototype.__proto__.print = function(){ console.log("Person prototype hacked");};
// создаем объект Person
const bob = new Person("Bob", 43);
bob.print();      // Person prototype hacked
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850