Hoje vou aqui divagar sobre o meu amor/ódio de estimação: o Javascript. Porque é que amo/detesto este monte de boas ideias/jorda? É, é difícil explicar mesmo.
Em parte a minha relação de amor explica-se pela falta de alternativas. Um pouco como quando se estuda numa universidade de engenharia em que apenas 10% dos estudantes são do sexo feminino e aí tenta-se encontrar a alma gémea menos feia que se consegue. Mas até aí tem mais escolha. No caso do Javascript, apesar de servir para muita coisa, é a única porra que corre scripting do lado do cliente nos navegadores (sim, tem implementações diferentes em cada navegador, tou ciente disso). No entanto, tem muito boas ideias/conceitos espalhados um pouco pela sua especificação, sim, muitos deles roubados, mas mesmo assim válidos. Um bom exemplo é essa prototipização, que foi a maneira de eles auxiliarem algo que se assemelhe a linguagem orientada a objectos, mantendo o formato mini da linguagem, fácil de aprender e acessível a profissionais e amadores.
Mas o que detesto são todas as incongruências que derivam de uma linguagem ter tipificação fraca e dinâmica. Essa combinação tóxica permite fazer coisas como:
var x = 2; var y = "2"; x == y;
dica: isto devolve true. Se isto não vos dá a volta ao estômago, então vocês são gente aventureira e amante do risco. Sugiro mudança de vida, risco não combina bem com a profissão de engenheiro.
No entanto, uma discussão recente no seio da equipa do projecto onde trabalho fez-me investigar mais um pouco dos conceitos da programação orientada a objectos em Javascript. Um dos meus colegas decidiu reorganizar o nosso código executado do lado do cliente no seu navegador, que consistia maioritariamente de funções de primeira ordem executadas mediante determinado contexto presente na estrutura HTML carregada, alguns widgets, entre outras salganhadas. Decidiu aplicar os conceitos de prototipização do Javascript, desenhando uma estrutura em que uma função define determinado tipo de prototipização, outra extende essa prototipização e depois passaa uma como protótipo da outra, de forma a criar uma relação de herança, em que um dos protótipos trata de funcionalidade geral, e outro de funcionalidade específica. Enfim, relativamente limpinho. Que são protótipos, devem estar vocês a perguntar? Bom, é complicado de entender, mas vamos lá tentar.
Em Javascript, existem essencialmente dois tipos de estruturas complexas (não ignoremos os tipos primitivos, claro): funções e objectos. Um objecto, que segue a notação de objecto Javascript (JSON para os conhecedores, outra das grandes ideias de estrutura e representação de dados que saiu do javascript), podemos definir como um saco de associações propriedade -> valor. Quase como um mapa de dados. Qual é aqui a nuance? o valor pode ser qualquer coisa, sendo esse qualquer coisa do tipo primitivo, outro objecto JSON ou uma função. Sendo que a definição da função dentro do objecto ocorre num âmbito específico, esta função somente poderá ser chamada ou dentro desse âmbito ou referenciando o objecto. Algo como isto:
var a = {
"palavra" : "diami",
"numero" : 2,
"b" : {"c" : 1},
"func" : function() {return "balls";}
};
a.palavra #=> "diami"
a.numero #=> 2
a.func #=> function()
a.func() #=> "balls"
Notem que à propriedade "func" foi atribuída uma função anónima. Sim, outras das nuances do Javascript é o suporte à definição de funções anónimas. O que me leva ao seguinte ponto: e o que são as funções em Javascript afinal? Pois, aqui é que tudo fica mais complicado. Vamos descomplicar.
Ao contrário de outras linguagens de programação, funções em Javascript são objectos de primeira ordem. Podem ser atribuídas a variáveis:
var a = function t() {return "bong";}; a() #=> "bong"
Podem chamar-se a si mesmas:
function () {return "balls"}(); #=> "balls"
Mas o mais misterioso de tudo, e a grande diferença em relação aos objectos JSON, é que todas as funções têm uma propriedade interna associada a que se convencionou chamar o seu protótipo (prototype). Podem acessar a mesma do seguinte modo:
var a = function() {}; a.prototype #=> {}
Mas esse protótipo não é um objecto normal. Por definição tem uma propriedade associada chamada 'constructor'. E o que é esse 'constructor' mesmo? Pois é, vamos ver:
var a = function() {}; a.prototype.constructor #=> function()
Concluímos que esse 'constructor' aponta para uma função. Mas que função é essa? Vamos explorar mais esta ideia:
var a = function balls() {}; a.prototype.constructor #=> balls()
Respiremos fundo... Então, a variável a tem uma função atribuída, que por sua vez tem uma propriedade chamada 'prototype', que por sua vez, tem uma propriedade chamada 'constructor', que por sua vez aponta para a função onde esse prototype está definido. Pura loucura!! Como poderá isto ser útil?
Mas é. Agora preparem-se. Vamos entrar no mundo do this.
this é uma palavra reservada em Javascript. Se escreverem this na consola do vosso browser vai aparecer a "window" (mais sobre o objecto window num outro post). Dentro de uma função podem também escrever e utilizar o this. E o que será o this nesse caso? O this não é mais do que o objecto/contexto no qual aquela função é executada. Logo:
function() {return this;}() #=> window
var a = {x : function() {return this.y;}, y : 2}; a.x() #=> 2
Agora o imbróglio fica mais claro. Vamos baralhar e dar agora.
Como sabem de outras linguagens, uma chamada a uma função dá-se recorrendo aos parênteses. Mas o Javascript tem mais de uma forma de chamar uma função:
var a = function() {return this;};
a() #=> window
a.call() #=> window
a.apply({x : 2}) #=> {"x" : 2}
Conclui-se que podemos passar explicitamente o contexto interno da função, ou se quisermos ser mais explícitos, o protótipo acessível da função, em contraponto ao tal protótipo inacessível do qual falámos antes. Onde é que estes se misturam?
Eis que chegamos à última peça do puzzle, a palavra reservada new. O new tem uma função muito particular no Javascript. Primeiro, não pode ser chamada em objectos JSON:
var a = new {a : 2}; #=> erro!!!
mas pode ser chamado numa função:
var a = new function() {return "balls";}; a; #=>
no entanto, nem lhe é atribuído o valor retornado pela função, nem este valor é retornado para a consola. Simplesmente perdeu-se. e a variável a passou a ter como valor um objecto JSON vazio. O que se passou então?
O que se passou foi que, quando se chama o new numa função, ocorrem os seguintes eventos:
1 - inicializa um objecto JSON vazio;
2 - este objecto é por sua vez interpolado com o protótipo da função;
3 - o construtor do protótipo é chamado utilizando como contexto o novo objecto interpolado.
E puf, fez-se programação orientada a objectos em Javascript. Ora brinquemos um pouco:
var Geral = function() {
this.atributoGeral1 = 1;
this.atributoGeral2 = 2;
}
var Especifico = function() {
this.atributoEspecifico1 = 3;
this.atributoEspecifico2 = 4;
}
Especifico.prototype = new Geral();
var variavelGeral = new Geral(),
variavelEspecifica = new Especifico();
variavelGeral.atributoGeral1 #=> 1
variavelGeral.atributoGeral2 #=> 2
variavelGeral.atributoEspecifico1 #=> undefined
variavelGeral.atributoEspecifico2 #=> undefined
variavelEspecifica.atributoGeral1 #=> 1
variavelEspecifica.atributoGeral2 #=> 2
variavelEspecifica.atributoEspecifico1 #=> 3
variavelEspecifica.atributoEspecifico2 #=> 4
O Javascript consegue ser tão horrivelmente simples, né? Essa magia negra descrita toda em cima permite que no final se consiga "simular" uma hierarquia de heranças perfeitamente fácil de seguir.
Qual foi o motivo da discussão descrita mais acima então?
Bom, o meu problema foi com a nomenclatura dada. O meu colega decidiu poluir o nome dado ás variáveis que definem as nossas "pseudo-classes" (não conheço a definição oficial, quem conhecer ajude). Em vez de algo relativamente simples de entender para quem vem de linguagens de herança por classes:
var Bola = function() {
this.cor = "azul_e_branca";
};
var joao = {
"bola" : new Bola()
}
joao.bola.cor #=> "azul_e_branca";
decidiu "sufixar" essas variáveis com "Constructor":
var BolaConstructor = function() {
this.cor = "azul_e_branca";
};
var joao = {
"bola" : new BolaConstructor()
}
joao.bola.cor #=> "azul_e_branca";
Eu achei aquilo semânticamente errado. Dá a entender que existe um construtor central de bolas, que quando chamado cospe bolas. É um pouco confuso de se ler. Mas o meu colega, que adora defender os seus cavalos de Tróia, insistia que aquela variável à qual é atribuída a função anónima É o construtor, e portanto tem que ser nomeada como tal. Mais tarde, mesmo depois de dar a mão à palmatória que realmente a nomenclatura tornava difícil a comprensão, insistia que essa função continuava de facto a ser o construtor. Ora, fui mais uma vez dar uma olhada na documentação, e basicamente respondo-lhe com ela assim que surgir a oportunidade: somente protótipos têm originalmente um construtor em Javascript; lá porque estes chamam a função em que existem, ou são passados com a mesma para o objecto criado com a chamada ao new, não faz das mesmas construtores, simplesmente são referenciados/chamados no contexto do protótipo.
Pelo menos é esta a minha leitura. Talvez vocês tenham outra.