Ao contrário do que muitos pensam, JavaScript permite sim orientação à objetos. Um tipo um tanto diferente é verdade (sem classes), por outro lado, permite coisas muito interessantes como mixins e outras mágicas típicas de linguagens dinâmicas.
Neste artigo vou mostrar como desenvolver em JavaScript utilizando orientação à objetos e outros truques de umas das linguagens mais utilizadas do mundo.
Classes
Em JavaScript não existe o conceito de classes como em Java ou C#, entretanto, é possível criar pseudo-classes, inclusive com suporte a herança. Para criar uma “classe” em JavaScript deve-se definir uma função construtor desta forma:
function Car(m, y) {
this.model = m;
this.year = y;
this.getAge = function() {
return (new Date()).getFullYear() - this.year;
}
}
Ao invocar esta função com o operador “new”, uma instância/objeto desta classe é criada:
var myCar = new Car("Nissan Skyline", 2008);
Diferentemente do que acontece em outras linguagens, em JavaScript a simples atribuição de um valor à uma variável já é suficiente para declará-la e iniciá-la. Note também que a palavra-chave “this” funciona de forma parecida com Java e C#, ou seja, refere-se à instância que está sendo criada. Desta forma podemos acessar as variáveis criadas pelo construtor como propriedades do objeto:
myCar.model // "Nissan Skyline"
myCar.year // 2008
myCar.getAge() // 2
Em nossa classe há um método chamado "getAge", este método é na verdade uma propriedade de cada objeto criado. Isso significa que cada objeto criado irá alocar uma parte da memória para armazená-lo, podendo consumir muita memória caso muitas instâncias desta classe sejam criadas. Para resolver este problema deve-se utilizar o prototype da classe para definir seus métodos.
Prototypes
Todo objeto em JavaScript possui uma referência interna a um objeto chamado “prototype”. Toda propriedade do prototype de uma classe é também uma propriedade dos objetos desta classe, ou seja, todo objeto herda as propriedades de seu prototype. Uma melhor versão da classe “Car” ficaria assim:
function Car(m, y) {
this.model = m;
this.year = y;
}
Car.prototype.getAge = function() {
return (new Date()).getFullYear() - this.year;
}
Desta forma continuamos podendo acessar o método “getAge”, porém, sem ter de alocá-lo para cada objeto. Note que a definição do método é feita depois da classe, isso demonstra que podemos adicionar membros a uma classe a qualquer momento através de seu prototype. Para determinar se uma propriedade foi definida em seu construtor ou herdada de seu prototype basta chamar a função “hasOwnProperty()” do objeto:
var c = new Car("Honda Civic", 1995);
c.hasOwnProperty("model"); // true
c.hasOwnProperty("getAge"); // false - Herdada do prototype
Propriedades e métodos estáticos
Propriedades e métodos de classe, também conhecidos como “estáticos”, são bastante utilizados em Java e C#. São membros que não estão em uma instância/objeto especifico, mas sim diretamente na classe. Um exemplo de classe que possui este tipo de propriedade/método é a classe “Math”, onde temos a propriedade “Math.PI”, que retorna a constante PI, e o método “Math.max(a, b)”, que retorna o maior dos dois argumentos.
Para criar um membros estático basta atribuí-los diretamente à classe:
Painter.paint = function(o, c)
{
o.color = c;
}
Painter.paint(car, "black");
car.color; // "black"
Herança
Como eu disse no início, em JavaScript não há o conceito de classes como em Java ou C#. Ainda assim, isso não significa que não seja possível estabelecer herança entre tipos. Suponha que agora precisamos criar um carro que armazene sua velocidade e possa ser acelerado. Para isso, vamos criar uma nova classe que extenda a classe “Car”:
function AccelerableCar(m, y, s){
Car.call(this, m, y); // chama o construtor da classe mãe
this.maxSpeed = s;
this.speed = 0;
}
AccelerableCar.prototype.accelerate = function(by){
this.speed = Math.min(this.speed + by, this.maxSpeed);
}
AccelerableCar.prototype = new Car(); // realiza a herança!
AccelerableCar.prototype.contructor = AccelerableCar; // a propriedade 'constructor' do prototype referencia a função construtor da classe
var ac = new AccelerableCar('Porshe Carrera', 2005, 350);
ac.model // 'Porshe Carrera'
ac.year // 2005
ac.speed // 0
ac.accelerate(150);
ac.speed // 150
Como você pode notar, criar uma sub-classe em JavaScript não é tão simples. Primeiro, em seu construtor é necessário ter certeza que o construtor da classe mãe é chamado através do método “call”. Em JavaScript toda função possui uma “sub-função” chamada “call”, esta função executa a função original trocando o objeto interno “this” pelo rimeiro argumento. Depois, deve-se atribuir ao prototype da classe filha uma instância da classe mãe, desta forma a classe filha adquire todos os membros da classe mãe.
Por fim, deve-se atribuir a propriedade “constructor” do prototype da classe filha a função construtor da mesma. Isso é importante pois ao executar esta linha “AccelerableCar.prototype = new Car()” o prototype de “AccelerableCar” é alterado para “Car” e seu construtor passa a ser o de “Car”.
Módulos, extendendo sem usar herança
Algumas linguagens de programação como, por exemplo, Ruby, dispõem de um recurso muito interessante chamado de módulo. Um módulo é bastante parecido com uma classe, pode conter métodos, propriedades e até classes, porém, não pode ser instanciado. Em contra-partida, uma classe pode “herdar” de quantos módulos quiser e, diferentemente do que acontece com interfaces, os médotos de um módulos são implementados no próprio módulo. Isso é conhecido como “mixin”, uma forma de fazer de herança múltipla, mas sem seus problemas.
Em JavaScript podemos simular módulos de forma parecida como fazemos para simular classes, com a diferênça de que por convensão os métodos de um módulo são estáticos, e seu construtor emite uma exceção caso seja invocado. Abaixo um exemplo de um módulo que define um método para serializar um objeto simples em JSON:
function Serializable(){
throw "You can’t create an object from a module!";
}
Serializable.serialize = function(o) {
obj = o || this;
var members = [];
for(m in obj) {
if(typeof obj[m] != "function")
members.push("'" + m + "':'" + obj[m].toString() + "'");
}
return "{" + members.join(",") + "}";
}
Agora podemos usar este módulo em qualquer classe que quisermos:
Car.prototype.serialize = Serializable.serialize;
var myCar = new Car("Porshe Carrera", 2010);
alert(myCar.serialize()); // {'model':'Porshe Carrera','year':'2010'}
Depois do mixin a classe classe “Car” passa a ter o médoto “serialize” de “Serializable”. Você também deve ter notado que este processo de mixin “manual” se torna inviável caso o módulo possua muitos métodos. Para resolver isso, podemos criar um helper para fazer o trabalho pesado:
function Module(){}
Module.mixin(source, target)
{
for(m in source) {
if(typeof source[m] == "function")
target.prototype[m] = source[m];
}
}
Module.mixin(Serializable, Car);
Namespaces
Uma boa-prática em programação orientada à objetos é definir namespaces para organizar o código e evitar conflitos, principalmente em projetos grandes. Ainda que não exista nenhum suporte à namespaces na linguagem JavaScript, é possível utilizar objetos para tal. No exemplo abaixo uma namespace "com.game" e define a classe “Car” dentro dela:
var com = {};
com.game = {};
com.game.Car = function(m, y) { /* ... */ }
var c = new com.game.Car("Audi TT", 2002);
Conclusão
JavaScript apesar não possuir suporte nativo a conceitos tradicionais de orientação à objetos, graças à sua natureza dinâmica e flexível, permite que tais funcionalidades sejam simuladas de maneira bastante satisfatória e, em alguns casos, melhor que em linguagens orientadas à objetos tradicionais.
Ainda que seja importante entender como tudo isso é possível, é importante também frisar que existem diversos frameworks JavaScript (como Ext JS, Dojo, Google Closure entre outros) que possuem helpers que facilitam em muito a criação de aplicações orientadas à objetos em JavaScript