Mecanismos de Protótipo e Herança em JavaScript

Fundamentos do Modelo de Objetos

JavaScript opera primariamente sobre um sistema baseado em protótipos, diferentemente de linguagens clássicas orientadas a objetos que utilizam classes como blueprint principal. Embora a especificação ES6 tenha introduizdo a palavra-chave class, esta funciona como açúcar sintático sobre a infraestrutura existente de protótipos. A estrutura fundamental permanece sendo o objeto.

Todo objeto em JavaScript possui uma ligação interna para outro objeto, conhecido como seu protótipo. Essa ligação permite que um objeto herde propriedades e métodos. Essa cadeia de ligações é denominada cadeia de protótipos. A busca por propriedades ocorre dinamicamente: se uma propriedade não é encontrada no objeto instanciada, o motor JavaScript consulta o protótipo daquele objeto, e subsequentemente o protótipo do protótipo, até encontrar a propriedade ou atingir o fim da cadeia, representado por null.

Navegação na Cadeia de Protótipos

A especificação ECMA define o slot interno [[Prototype]] para representar essa ligação. No ambiente de execução, podemos acessar essa referência através de Object.getPrototypeOf(). Historicamente, muitos navegadores expuseram a propriedade não padrão __proto__, que cumpre função similar, embora seu uso seja desencorajado em produção em favor dos métodos padrão.

É crucial distinguir entre o protótipo de uma instância e a propriedade prototype de uma função consrtutora. A propriedade prototype existe apenas em funções destinadas a serem construtoras e define o objeto que será atribuído como protótipo às instâncias criadas por aquela função.

const configuracaoBase = { idioma: 'pt-BR', tema: 'claro' };
const preferenciasUsuario = { tema: 'escuro' };

// Estabelecendo a cadeia de protótipos
Object.setPrototypeOf(preferenciasUsuario, configuracaoBase);

console.log(Object.getPrototypeOf(preferenciasUsuario)); // { idioma: 'pt-BR', tema: 'claro' }
console.log(preferenciasUsuario.idioma); // 'pt-BR' (herdado)
console.log(preferenciasUsuario.tema);   // 'escuro' (próprio, sobrescreve o herdado)
console.log(Object.getPrototypeOf(Object.getPrototypeOf(preferenciasUsuario))); // Object.prototype

Contexto de Execução e Herança de Métodos

Em JavaScript, funções são cidadãs de primeira classe e podem ser atribuídas como propriedades de objetos. Quando um método é herdado via cadeia de protótipos e executado, o valor de this é determinado pelo objeto que invocou o método, não pelo objeto onde o método foi originalmente definido. Isso permite reutilização de código com contextos diferentes.

const calculadora = {
    valor: 10,
    adicionar: function(qtd) {
        return this.valor + qtd;
    }
};

const contaEspecial = Object.create(calculadora);
contaEspecial.valor = 50;

console.log(calculadora.adicionar(5));      // 15 (this aponta para calculadora)
console.log(contaEspecial.adicionar(5));    // 55 (this aponta para contaEspecial)

Padrões de Criação de Objetos

Existem múltiplas formas de instanciar objetos e estabelecer suas relações de protótipo, cada uma com implicações específicas para a memória e a cadeia de herança.

1. Literais e Construtores

Objetos literias herdam diretamente de Object.prototype. Funções construtoras, quando invocadas com new, criam instâncias cujo protótipo interno aponta para a propriedade prototype da construtora.

function GestorTarefas() {
    this.tarefas = [];
}

GestorTarefas.prototype.adicionar = function(tarefa) {
    this.tarefas.push(tarefa);
};

const gestor = new GestorTarefas();
// gestor.__proto__ === GestorTarefas.prototype

2. Object.create

O método Object.create() permite especificar explicitamente qual objeto servirá como protótipo para o novo objeto criado, oferecendo controle fino sobre a cadeia de herança sem invocar construtores.

const modelo = { ativo: true };
const instancia = Object.create(modelo);

console.log(instancia.ativo); // true (herdado)
console.log(instancia.hasOwnProperty('ativo')); // false

3. Sintaxe Class (ES6)

A sintaxe class moderniza a declaração de construtores e protótipos, mas internamente continua utilizando o mecanismo de protótipos. O comando extends configura a cadeia de protótipos entre as classes.

class Forma {
    constructor(cor) {
        this.cor = cor;
    }
    descrever() {
        return `Forma ${this.cor}`;
    }
}

class Circulo extends Forma {
    constructor(cor, raio) {
        super(cor);
        this.raio = raio;
    }
}

Padrões de Herança em JavaScript

Antes da padronização das classes ES6, desenvolvedores utilizavam diversos padrões para simular herança clássica. Compreender esses padrões é vital para manter código legado e entender o comportamento interno da linguagem.

Herança via Cadeia de Protótipos

Neste modelo, o protótipo da subclasse é definido como uma instância da superclasse. Isso permite que métodos definidos no protótipo da superclasse sejam acessíveis pela subclasse.

function Animal() {
    this.especie = 'genérica';
}
Animal.prototype.dormir = function() {
    console.log('Dormindo...');
};

function Cachorro() {
    this.raca = 'vira-lata';
}

// Herança estabelecida
Cachorro.prototype = new Animal();
Cachorro.prototype.constructor = Cachorro;

Cachorro.prototype.latir = function() {
    console.log('Au Au');
};

const dog = new Cachorro();
dog.dormir(); // Funciona, herdado de Animal

Uma limitação significativa deste approach é o compartilhamento de propriedades de tipo referência. Se a superclasse possuir um array ou objeto em suas propriedades de instância, todas as subclasses compartilharão a mesma referência, levando a efeitos colaterais indesejados.

Herança via Empréstimo de Construtor

Para evitar o compartilhamento de propriedades de referência, pode-se invocar a construtora da superclasse dentro da construtora da subclasse usando call ou apply. Isso garante que cada instância tenha sua própria cópia das propriedades.

function Pessoa(nome) {
    this.nome = nome;
    this.hobbies = [];
}

function Desenvolvedor(nome, linguagem) {
    Pessoa.call(this, nome); // Copia propriedades de instância
    this.linguagem = linguagem;
}

const dev1 = new Desenvolvedor('Carlos', 'JS');
const dev2 = new Desenvolvedor('Maria', 'Python');

dev1.hobbies.push('Coding');
// dev2.hobbies permanece vazio, não há compartilhamento

Contudo, este método não herda métodos definidos no protótipo da superclasse, apenas as propriedades de instância.

Herança Combinada

A herança combinada utiliza ambas as técnicas anteriores: empréstimo de construtor para propriedades de instância e cadeia de protótipos para métodos. Isso resolve o problema das referências compartilhadas e permite a reutilização de métodos.

function Veiculo(marca) {
    this.marca = marca;
    this.peças = ['motor'];
}
Veiculo.prototype.mover = function() { console.log('Movendo'); };

function Carro(marca, modelo) {
    Veiculo.call(this, marca); // Herança de propriedades
    this.modelo = modelo;
}

// Herança de métodos
Carro.prototype = new Veiculo();
Carro.prototype.constructor = Carro;

Carro.prototype.buzinar = function() { console.log('Buzina'); };

A desvantagem aqui é que a construtora da superclasse (Veiculo) é executada duas vezes: uma durante o empréstimo (call) e outra durante a configuração do protótipo (new Veiculo()).

Herança Combinada Parasítica

Este padrão otimiza a herança combinada evitando a execução dupla da construtora da superclasse. Utiliza-se uma função intermediária para criar um objeto que herda do protótipo da superclasse sem instanciá-la completamente, corrigindo em seguida o construtor.

function herdarPrototipo(Sub, Super) {
    function Temp() {}
    Temp.prototype = Super.prototype;
    Sub.prototype = new Temp();
    Sub.prototype.constructor = Sub;
}

function Entidade(id) {
    this.id = id;
    this.dados = {};
}
Entidade.prototype.salvar = function() { console.log('Salvo'); };

function Usuario(id, nome) {
    Entidade.call(this, id);
    this.nome = nome;
}

herdarPrototipo(Usuario, Entidade);
Usuario.prototype.bemVindo = function() { console.log('Olá'); };

const user = new Usuario(1, 'Ana');
user.salvar(); // Herdado eficientemente

Esta abordagem garante que métodos sejam compartilhados via protótipo, propriedades de instância sejam isoladas e a construtora pai seja chamada apenas uma vez durante a instanciação do objeto final.

Tags: javascript prototype-chain inheritance-patterns ecmascript OOP

Publicado em 9-5 13:25