В прошлой теме было рассмотрено наследование объектов или точнее их прототипов. Использование функций-конструкторов делает шаг вперед в этом плане, позволяя наследовать прототипы в псевдоклассовом стиле, как наследование типов.
Например, у нас может быть объект 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