Herança em JavaScript parte I

Herança em JavaScript parte I

Diferente das linguagens mais conhecidas, como Java ou C++ que utilizam a orientação a objetos clássica, JavaScript utiliza uma abordagem diferente para compartilhar código entre entidades, chamada de orientação a protótipo. Mas antes de entrarmos em detalhes, vamos primeiro relembrar o que é a herança clássica nas linguagens orientadas a objetos.

Herança clássica

Herança em OOP é a capacidade de classes compartilharem atributos e métodos entre si. Geralmente a herança é usada para compartilhar comportamentos generalizados e comuns entres as classes filhas.

Show me the code

Imaginem que temos que representar em nossa aplicação alguns animais, como gato e cachorro. Com herança podemos definir comportamentos comuns entre eles apenas uma vez, como respirar, nascer e morrer, e reutilizar estes métodos já implementados apenas os herdando nas classes filhas.

Vamos implementar o cenário acima em Java. Como desejamos representar gatos e cachorros, e sabemos que eles possuem métodos similares, como nascer, morrer e respirar, vamos criar uma classe Animal e implementar estes métodos nela, para que gatos e cachorros possam herdá-los.

// setamos a classe como abstrata pois não desejamos criar
// objetos do tipo animal, apenas os tipos mais
// especializados como Gato ou Cachorro podem ser instânciados
public abstract class Animal {
  public void nascer() {
    // ...
  }

  public void morrer() {
    // ...
  }

  public void respirar() {
    // ...
  }
}

Agora definimos as classes Gato e Cachorro, que irão herdar de Animal e implementar métodos que só fazem sentido em seu próprio escopo, como miar no caso de Gato e latir no caso de Cachorro.

// além do método miar, os objetos do tipo Gato
// terão também, devido a herança, os métodos de Animal
class Gato extends Animal {
  public void miar() {
    // ...
  }

  // construtor
  public Gato() {
    // ...
  }
}

// com os objetos do tipo Cachorro acontece o mesmo
// além do método latir, definido explicitamente na classe
// ele irá herdar os métodos nascer, morrer e respirar
class Cachorro extends Animal {
  public void latir() {
    // ...
  }

  // construtor
  public Cachorro() {
    // ...
  }
}

Definimos a relação de herança com a palavra reservada extends. Com isso as classes Gato e Cachorro irão possuir, além de seus próprios métodos, os métodos herdados de Animal, como nascer, morrer e respirar.

Então fica claro que com herança conseguimos um enorme reuso de código. De outra forma, teríamos que redefinir métodos e atributos comuns em todas as classes, tornando a manutenção do código mais complexa e propensa a erros.

Cadeia de protótipos

Em JavaScript (quase) tudo é um objeto, não existem classes. Até mesmo as function são objetos. Se quisermos herdar os métodos e atributos de um objeto, o utilizamos como protótipo do novo objeto a ser criado. Mesmo que não esteja definido explicitamente no código, todos os outros objetos de JavaScript, com a exceção do objeto Object, utilizam um outro objeto como protótipo.

Como no exemplo abaixo, definimos um objeto vazio, um array vazio e uma função. Eles todos herdarão métodos e atributos de seus protótipos.

// aqui criamos um novo objeto genérico com o nome carro
// ele automaticamente usará o prototipo de Object
var carro = {}
carro.modelo = 'Celta'
carro.marca = 'Chevrolet'
carro.hasOwnProperty('modelo') // método herdado de Object
Object.getPrototypeOf(carro) // retorna Object

// frutas herdará as propriedades de Array
var frutas = [ 'morango', 'manga', 'laranja' ]
frutas.reverse() // método herdado de Array
frutas.hasOwnProperty('length') // método herdado de Object
Object.getPrototypeOf(frutas) // retorna []
Object.getPrototypeOf(Object.getPrototypeOf(frutas)) // retorna Object

// validarCPF irá herdar propriedades de Function
function validarCPF() {
  // ...
  return true
}
// aqui vemos uma função se comportando como um objeto
validarCPF.call() // método herdado de Function
Object.getPrototypeOf(validarCPF) // retorna function Empty() {}

É interessante notar que a herança ocorre em toda a cadeia de protótipos. Como quando definimos um array: ele herda todas as propriedades de Array e de Object, uma vez que Array usa Object como protótipo. Podemos verificar isso chamando o protótipo do protótipo de frutas. O protótipo de frutas é Array, e o protótipo de Array é Object ou, o protótipo do protótipo de frutas é Object.

Quando chamamos a propriedade de um objeto, o interpretador/VM JavaScript primeiro procura esta propriedade dentro do objeto, caso não encontre procura em seu protótipo, caso não encontre novamente procura no protótipo do protótipo, e assim sucessivamente, percorrendo toda a cadeia de protótipos até alcançar um protótipo com valor null.

Herança em JavaScript com construtores

O método mais difundido e crossbrowser de criação de objetos e herança em JavaScript é através de funções que funcionam como construtores. Nesse método, definimos funções que irão se comportar como construtores em linguagens clássicas orientadas a objeto. Depois de definida a função, podemos instanciar objetos do tipo definido usando new.

Em JavaScript, uma função também é um objeto, e ela possui a propriedade prototype. Nesta propriedade definimos o prototipo da função, ou todas as propriedades que os objetos deste tipo irão ter se invocarmos new.

Ok, pode parecer complicado falando, mas fica bem fácil olhando o código.

// criamos o construtor Animal
function Animal() {
}
Animal.prototype.nascer = function() {
  // ...
}
Animal.prototype.morrer = function() {
  // ...
}
Animal.prototype.respirar = function() {
  // ...
}

Agora definimos Gato e Cachorro, e usamos Animal como protótipo.

// criamos o construtor Gato
function Gato(nome) {
  this.nome = nome
}
Gato.prototype = new Animal() // definimos que Gato usa Animal como protótipo
Gato.prototype.constructor = Gato // para que não fique com o valor do construtor do objeto usado como protótipo
Gato.prototype.miar = function() { // método miar apenas para Gato
  // ...
}

// criamos o construtor Cachorro
function Cachorro(nome) {
  this.nome = nome
}
Cachorro.prototype = new Animal() // definimos que Cachorro usa Animal como protótipo
Cachorro.prototype.constructor = Cachorro()
Cachorro.prototype.latir = function() {
  // ...
}

var rex = new Cachorro('rex') // criamos um objeto do tipo Cachorro
rex.latir() // utilizando um método definido em Cachorro
rex.respirar() // utilizando um método herdado de Animal
rex.nome // retorna 'rex'

Precisamos criar um novo objeto do tipo Animal para setarmos como protótipo, pois caso contrário estariamos passando a referência para a função.

A propriedade constructor nos informa o construtor do objeto. Nós precisamos defini-la manualmente pois esta proprieda existe no objeto Animal que passamos por protótipo, então todos os objetos de Cachorro e Gato irão herdar a propriedade construtor com o setado valor como Animal. Esta propriedade pode ser útil caso seja preciso chamar um método de um dos objetos na cadeia de protótipos.

Podemos verificar que rex é de fato um cachorro perguntado se ele é uma instância de Cachorro.

rex instanceof Cachorro // retorna true
rex instanceof Animal // retorn true. Cachorro.prototype --> Animal
rex instanceof Object // retorna true. Cachorro.prototype --> Animal.prototype --> Object
rex instanceof Array // retorna false

Interessante notar que rex também é uma instância de Animal, já que Cachorro usa Animal como protótipo. E também é instância de Object, já que Animal usa, implicitamente, Object como protótipo.

Por ser uma linguagem orientada a protótipos, nós podemos definir um novo ao método ao protótipo e todos os objetos já instanciados irão ter acesso a este método criado, o que é impossível de ser feito em linguagens como Java (me corrijam se estiver falando besteira).

Cachorro.prototype.morder = function() {
  // ...
}
rex.morder() // rex mesmo depois de instanciado terá acesso aos novos métodos definidos no protótipo de Cachorro

Um dos perigos dessa abordagem é que caso se esqueça de usar new, o this dentro da function irá se referenciar ao objeto global, e poderá sobreescrever algumas variáveis já declaradas antes.

Eu particularmente não sou a favor do uso do new pois ele torna ambíguo o uso de funções. Pois algumas funcionam como construtores e outras como funções normais. E sintaticamente o new pode ser usado em qualquer tipo de função.

Continua…

Este é o método padrão de herança em JavaScript. Grande parte desta bagunça é devido ao JavaScript ter sido lançado muito às pressas. Porém o ECMAscript, o grupo que padroniza o JavaScript, vem adicionado várias funções para tornar o trabalho com herança mais simples e finalmente abraçando a orientação a protótipo.

E é sobre este novo método de trabalhar com herança no ECMAscript 5 que iremos discutir na 2ª parte do post.

#5