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.