Moștenirea prototipurilor în cazul constructorilor
În tema anterioară, am discutat despre moștenirea obiectelor sau, mai exact, a prototipurilor lor. Utilizarea funcțiilor constructor face un pas înainte în acest sens, permițând moștenirea prototipurilor într-un stil pseudo-clasă, similar moștenirii de tipuri.
De exemplu, putem avea un obiect "Person", care reprezintă un utilizator individual. De asemenea, putem avea un obiect "Employee", care reprezintă un angajat. Dar un angajat poate, de asemenea, să fie un utilizator, și, prin urmare, ar trebui să aibă toate proprietățile și metodele acestuia. De exemplu:
// Constructorul pentru utilizator
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function () {
console.log(`Person ${this.name} says "Hello"`);
};
}
// Adăugăm prototipul în funcție
Person.prototype.print = function () {
console.log(`Name: ${this.name} Age: ${this.age}`);
};
// Constructorul pentru angajat
function Employee(name, age, comp) {
Person.call(this, name, age); // Aplicăm constructorul Person
this.company = comp;
this.work = function () {
console.log(`${this.name} works at ${this.company}`);
};
}
// Moștenim prototipul de la Person
Employee.prototype = Object.create(Person.prototype);
// Setăm constructorul
Employee.prototype.constructor = Employee;
Aici, la început, este definită funcția constructor pentru "Person", care reprezintă un utilizator. În "Person", sunt definite două proprietăți și două metode. Unul dintre metode, "sayHello", este definit în interiorul constructorului, în timp ce celălalt, "print", este definit direct în prototip.
Apoi, este definită funcția constructor pentru "Employee", care reprezintă un angajat.
În constructorul "Employee", se apelează constructorul "Person" folosind "call":
Person.call(this, name, age);
Prin transmiterea primului parametru, se permite apelul funcției constructor "Person" pentru obiectul creat de constructorul "Employee". Datorită acestui lucru, toate proprietățile și metodele definite în constructorul "Person" sunt, de asemenea, transferate la obiectul "Employee". În plus, este definită o proprietate suplimentară, "company", care reprezintă compania angajatului, și o metodă numită "work".
De asemenea, este necesar să se moștenească și prototipul "Person" și, implicit, toate funcțiile definite prin intermediul prototipului (de exemplu, în exemplul de mai sus, este funcția "Person.prototype.print"). Aceasta se realizează prin apelul:
Employee.prototype = Object.create(Person.prototype);
Metoda Object.create() permite crearea unui obiect cu prototipul "Person", care este ulterior atribuit prototipului "Employee".
Adesea, în loc să se folosească apelul metodei Object.create() pentru a stabili prototipul, se utilizează apelul constructorului moștenit, de exemplu:
Employee.prototype = new Person();
Ca rezultat, va fi creat un obiect ale cărui proprietăți prototipale (Employee.prototype.__proto__) vor indica către prototipul lui "Person".
Totuși, merită de menționat că obiectul creat va indica către constructorul "Person". De aceea, este important să setăm constructorul corespunzător:
Employee.prototype.constructor = Employee;
Constructorul este rar utilizat în mod izolat, iar absența setării constructorului poate să nu aibă nicio influență asupra funcționării programului. Cu toate acestea, să examinăm următoarea situație.
const obj = new Employee.prototype.constructor("Bob", 23, "Google");
console.log(obj); // Employee sau Person în funcție de tipul constructorului
obj.work(); // Dacă obj este de tipul Person, atunci va apărea o eroare
Aici, constructorul este apelat direct pentru a crea obiectul "obj". Tipul obiectului "obj" depinde de constructorul setat pentru "Employee.prototype.constructor".
Hai să testăm funcțiile constructor definite mai sus:
// Constructor pentru utilizator
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}`);
};
// Constructor pentru angajat
function Employee(name, age, comp) {
Person.call(this, name, age); // Aplicăm constructorul Person
this.company = comp;
this.work = function () {
console.log(`${this.name} works at ${this.company}`);
};
}
// Moștenim prototipul de la Person
Employee.prototype = Object.create(Person.prototype);
// Setăm constructorul
Employee.prototype.constructor = Employee;
// Creăm obiectul Employee
const tom = new Employee("Tom", 39, "Google");
// Accesarea proprietății moștenite
console.log("Age:", tom.age);
// Accesarea metodei moștenite
tom.sayHello(); // Person Tom says "Hello"
// Accesarea metodei moștenite din prototip
tom.print(); // Name: Tom Age: 39
// Accesarea metodei proprii
tom.work(); // Tom works at Google
Suprascrierea funcțiilor
În timpul moștenirii, putem suprascrie funcționalitățile moștenite. De exemplu, în exemplul anterior pentru clasa "Person" sunt definite două metode: "sayHello" (în constructor) și "print" (în prototip). Însă, să presupunem că pentru clasa "Employee" dorim să modificăm logica acestora, de exemplu, în metoda "print" să afișăm și compania angajatului. În acest caz, putem defini pentru clasa "Employee" metode cu aceleași nume.
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;
// Suprascriem metoda sayHello
this.sayHello = function () {
console.log(`Employee ${this.name} says "Hello"`);
};
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
// Suprascriem metoda 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
Metoda sayHello() este definită în interiorul constructorului Person, astfel că această metodă este suprascrisă în interiorul constructorului Employee. Metoda print() este definită ca metodă a prototipului Person, astfel încât poate fi suprascrisă în prototipul Employee.
Apelarea metodei din prototipul părinte
Într-un prototip copil, poate fi necesar să se apeleze o metodă din prototipul părinte. De exemplu, acest lucru poate fi necesar pentru a reduce duplicarea logicii codului, în special atunci când logica metodei din prototipul copil se repetă în mod similar cu logica metodei din prototipul părinte. În acest caz, pentru a accesa metodele prototipului părinte, se folosește funcția 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;
// Suprascriem metoda print
Employee.prototype.print = function () {
Person.prototype.print.call(this); // Apelăm metoda print din Person
console.log(`Company: ${this.company}`);
};
const tom = new Employee("Tom", 39, "Google");
tom.print(); // Name: Tom Age: 39
// Company: Google
În acest caz, în momentul suprascrierii metodei print în prototipul Employee, se apelează metoda print din prototipul Person.
Employee.prototype.print = function () {
Person.prototype.print.call(this); // Apelăm metoda print din Person
console.log(`Company: ${this.company}`);
};
Problemele cu ereditatea prototipală
Este important să observăm că tipul Employee nu moștenește doar toate proprietățile și metodele actuale din prototipul Person, ci și pe cele care vor fi adăugate ulterior în mod dinamic. De exemplu:
const tom = new Employee("Tom", 39, "Google");
Person.prototype.sleep = function() {console.log(${this.name} sleeps);}
tom.sleep();
Aici, în prototipul Person, se adaugă metoda sleep. Notați că aceasta este adăugată după ce obiectul tom a fost deja creat, reprezentând tipul Employee. Cu toate acestea, chiar și pentru acest obiect, putem apela metoda sleep.
Un alt aspect important de luat în considerare este că, prin intermediul prototipului constructorului-copil, putem schimba prototipul constructorului-părinte. De exemplu:
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;
}
// Moștenim prototipul de la Person
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
// Modificăm metoda print în prototipul de bază Person
Employee.prototype.__proto__.print = function () {
console.log("Person prototype hacked");
};
// Creăm un obiect Person
const bob = new Person("Bob", 43);
bob.print(); // Person prototype hacked