Heredança de Protótipos de Construtores - 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);

A passagem do 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.

Política de Privacidade

Copyright © www.programicio.com Todos os direitos reservados

É proibida a reprodução do conteúdo desta página sem autorização prévia do autor.

Contato: programicio@gmail.com