Herança de Protótipos de Construtores em JavaScript
Em JavaScript, a herança de objetos é implementada por meio de protótipos. O uso de funções construtoras permite a herança de protótipos em um estilo pseudoclássico, que se assemelha à herança de tipos em outras linguagens de programação.
Por exemplo, considere um objeto Person
, que representa um usuário individual. Também podemos ter um objeto Employee
, que representa um trabalhador. Como um trabalhador é também um usuário, ele deve herdar todas as propriedades e métodos do objeto Person
.
// construtor de usuário
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function () {
console.log(`Person ${this.name} says "Hello"`);
};
}
// adicionando protótipo à função
Person.prototype.print = function () {
console.log(`Name: ${this.name} Age: ${this.age}`);
};
// construtor de trabalhador
function Employee(name, age, comp) {
Person.call(this, name, age); // aplicando o construtor Person
this.company = comp;
this.work = function () {
console.log(`${this.name} works in ${this.company}`);
};
}
// herdando o protótipo de Person
Employee.prototype = Object.create(Person.prototype);
// definindo o construtor
Employee.prototype.constructor = Employee;
Inicialmente, a função construtora Person
é definida, representando o usuário. Em Person
, são definidas duas propriedades e dois métodos. Por exemplo, um método, sayHello
, é definido dentro do construtor, e outro método, print
, é definido diretamente no protótipo.
Em seguida, define-se a função construtora Employee
, que representa o trabalhador.
No construtor Employee
, ocorre uma chamada ao construtor Person
usando:
Person.call(this, name, age);
O primeiro parâmetro permite chamar a função construtora Person
para o objeto criado pelo construtor Employee
. Isso garante que todas as propriedades e métodos definidos no construtor Person
também sejam transferidos para o objeto Employee
. Além disso, define-se a propriedade company
, que representa a empresa do trabalhador, e o método work
.
Além disso, é necessário herdar também o protótipo Person
e, consequentemente, todas as funções definidas através do protótipo (por exemplo, a função Person.prototype.print
mencionada acima). Para isso, usa-se o comando:
O método Object.create()
permite criar um objeto protótipo de Person
, que é então atribuído ao protótipo de Employee
.
Muitas vezes, em vez de usar o método Object.create()
para definir um protótipo, é utilizado o construtor herdado, como por exemplo:
Employee.prototype = new Person();
Como resultado, será criado um objeto cujo protótipo (Employee.prototype.__proto__
) apontará para o protótipo Person
.
No entanto, é importante considerar que o objeto protótipo criado apontará para o construtor Person
. Por isso, também se define o construtor apropriado:
Employee.prototype.constructor = Employee;
O construtor raramente é usado por si só, e, possivelmente, a falta de definição do construtor não afetará o funcionamento do programa. No entanto, consideremos a seguinte situação:
const obj = new Employee.prototype.constructor("Bob", 23, "Google");
console.log(obj); // Employee ou Person, dependendo do tipo de construtor
obj.work(); // Se obj é Person, haverá um erro
Vamos testar as funções construtoras definidas acima:
// construtor de usuário
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}`);
};
// construtor de trabalhador
function Employee(name, age, comp) {
Person.call(this, name, age); // aplicando o construtor Person
this.company = comp;
this.work = function () {
console.log(`${this.name} works in ${this.company}`);
};
}
// herdando o protótipo de Person
Employee.prototype = Object.create(Person.prototype);
// definindo o construtor
Employee.prototype.constructor = Employee;
// criando objeto Employee
const tom = new Employee("Tom", 39, "Google");
// acessando a propriedade herdada
console.log("Age:", tom.age);
// acessando o método herdado
tom.sayHello(); // Person Tom says "Hello"
// acessando o método do protótipo herdado
tom.print(); // Name: Tom Age: 39
// acessando o método próprio
tom.work(); // Tom works in Google
Reescrita de Funções
Durante a herança, podemos reescrever a funcionalidade herdada. Por exemplo, no exemplo acima para Person
, são definidos dois métodos: sayHello
(no construtor) e print()
(no protótipo). No entanto, para Employee
, podemos querer alterar a lógica deles, por exemplo, no método print
, também mostrar a empresa do trabalhador. Nesse caso, podemos definir métodos para Employee
com os mesmos nomes:
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;
// reescrevendo o método sayHello
this.sayHello = function () {
console.log(`Employee ${this.name} says "Hello"`);
};
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
// reescrevendo o método 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
O método sayHello()
é definido dentro do construtor Person
, portanto, esse método é reescrito dentro do construtor Employee
. O método print()
é definido como um método do protótipo de Person
, portanto, pode ser reescrito no protótipo de Employee
.
Chamada do Método do Protótipo Parental
No protótipo herdeiro, pode ser necessário chamar um método do protótipo parental. Por exemplo, isso pode ser necessário para simplificar a lógica do código, se a lógica do método do herdeiro repetir a lógica do método parental. Nesse caso, para acessar os métodos do protótipo parental, usa-se a função 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;
// reescrevendo o método print
Employee.prototype.print = function () {
Person.prototype.print.call(this); // chamando o método print de Person
console.log(`Company: ${this.company}`);
};
const tom = new Employee("Tom", 39, "Google");
tom.print(); // Name: Tom Age: 39
// Company: Google
Neste caso, ao reescrever o método print no protótipo de Employee
, chama-se o método print
do protótipo de Person
:
Employee.prototype.print = function () {
Person.prototype.print.call(this); // chamando o método print de Person
console.log(`Company: ${this.company}`);
};
Problemas de Herança Prototípica
Vale ressaltar que o tipo Employee
herda não apenas todas as propriedades e métodos atuais do protótipo Person
, mas também aqueles que serão adicionados dinamicamente posteriormente. Por exemplo:
const tom = new Employee("Tom", 39, "Google");
Person.prototype.sleep = function () {
console.log(`${this.name} sleeps`);
};
tom.sleep();
Aqui, o método sleep
é adicionado ao protótipo Person
. Embora seja adicionado após a criação do objeto tom
, que representa o tipo Employee
, ainda é possível chamar o método sleep
neste objeto.
Outro ponto a considerar é que através do protótipo do construtor herdeiro, pode-se alterar o protótipo do construtor parental. Por exemplo:
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;
}
// herdando o protótipo de Person
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
// alterando o método print no protótipo básico de Person
Employee.prototype.__proto__.print = function () {
console.log("Person prototype hacked");
};
// criando um objeto Person
const bob = new Person("Bob", 43);
bob.print(); // Person prototype hacked
Este exemplo ilustra como as mudanças no protótipo de um construtor herdeiro podem afetar o protótipo do construtor parental, levando a resultados potencialmente inesperados e indesejados.