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