Linguagens e Ambientes de Programação (2016/2017)



Teórica 19 (16/mai/2017)

Programação orientada pelos objetos em JavaScript.



Objetos literais

Em JavaScript, os objetos são praticamente simples dicionários, que mapeiam etiquetas em valores. Existe uma sintaxe própria para definir objetos diretamente.

Eis um exemplo de objeto literal, que define uma pessoa:

Para aceder a uma componente dum objeto, há duas notações disponíveis: Se atribuirmos a um membro inexistente dum objeto, esse membro passa imediatamente a existir para esse objeto individual: Para apagar um membro, usa-se a palavra delete: Eis um objeto mais complexo:

Agora uma definição mais rigorosa e completa de objeto: um objeto é um dicionário enriquecido por uma propriedade privada especial que se chama prototype.


Programação baseada em protótipos

Para criar múltiplos objetos semelhantes e para reutilizar código, o JavaScript não usa classes mas sim protótipos. Esta técnica foi inventada em meados dos anos 1980 no contexto da linguagem Self.

Comecemos por falar um pouco da linguagem Self.

Em Self, a criação de novos objetos é efetuada a partir de objetos existentes. Sempre que um objeto P é usado como base para a criação de novos objetos, diz-se que P é um protótipo.

A criação dum novo objeto a partir dum protótipo P (designemos a operação por copy(P)) é muito simples: cria-se um objeto vazio (sem propriedades) e faz-se a propriedade especial prototype do novo objeto referir o protótipo P. Todos os objetos criados a partir dum protótipo P começam vazios e ficam a referir esse mesmo P.

Cada objeto herda dinamicamente do respetivo protótipo. A herança funciona assim: quando se tenta aceder a um membro dum objeto, se esse membro não estiver diretamente disponível no objeto, então a procura continua no respetivo protótipo. Se também não estiver diretamente disponível no protótipo, então procura-se no protótipo do protótipo. E assim sucessivamente, ao longo duma cadeia de protótipos.

Note que qualquer objeto pode ser usado como protótipo. Qualquer objeto passa a ser considerado um protótipo a partir do momento em que é usado para criar novos objetos.

Agora regressemos à linguagem JavaScript.

Em JavaScript, os objeto são idênticos aos do Self na medida em que contêm uma propriedade especial que identifica um protótipo, e cada objeto herda dinamicamente do seu protótipo. No entanto, o mecanismo disponível para a criação de objetos é mais complicado do que o do Self (veremos esse mecanismo na secção seguinte).

Em JavaScript, o protótipo dum objeto é guardado na seguinte propriedade privada:

Algumas implementações de JavaScript expõem essa propriedade através da propriedade pública __proto__. Nas implementações de JavaScript usadas na nossa cadeira (Rhino e NodeJS), esta propriedade pública está disponível e é usada como se exemplifica: De qualquer maneira, a maneira padronizada de testar se proto é protótipo de obj, é a seguinte: Todos os objetos definidos através dum literal partilham automaticamente um protótipo predefinido que se escreve: Veja esta pequena sessão interativa que prova de duas maneiras diferentes que os objetos literais herdam realmente de Object.prototype:

Programação orientada pelos objetos usando construtores

Em JavaScript, qual o mecanismo previsto para criar novos objetos e lhes associar o protótipo de onde herdam?

Convém começar por dizer que o mecanismo usado no JavaScript é um pouco complicado e foge ao que é tradicional nas linguagem baseadas em protótipos (que costumam imitar o Self). Foi provavelmente a pensar nos programadores de Java que se decidiu introduzir um mecanismo com alguma aparência de familiaridade.

O mecanismo usado em JavaScript para criar objetos é o mecanismo dos construtores. Um construtor serve para inicializar diversos objetos do mesmo tipo, que herdam do mesmo protótipo.

Um construtor C é um função com as seguintes particularidades:

Regra geral, os campos funcionais são adicionados no protótipo do construtor, para ficarem acessíveis por herança. Quando se adicionam novas funções ao protótipo, ele deixa de ser vazio. Repare bem: geralmente, os campos de dados pertencem a cada objeto e não são partilhados; os campos funcionais são metidos no protótipo e são partilhados por herança (pois não vale a pena repeti-los em cada objeto).

Abaixo define-se um construtor chamado Car para representar e inicializar automóveis. Neste exemplo, cada objeto fica com três campos de dados próprios. Os campos funcionais são seguidamente instalados no protótipo de Car e ficam disponíveis através de herança. Veja tudo com atenção:

Para aceder ao construtor dum objeto obj escreve-se:

Para aceder ao protótipo dum construtor C escreve-se:

Para testar se o construtor dum objeto obj é C, escreve-se:

Criação de hierarquias através dos construtores

Manipulando diretamente o membro prototype dos construtores é possível criar uma hierarquia de protótipos.

Eis um exemplo simples, que introduz um subtipo de Car. Repare que se muda o protótipo de FlyingCar para ser um objeto de tipo Car (em vez do habitual objeto vazio inicial).

Para tirar possível dúvidas sobre o mecanismo de herança, estudemos a seguinte chamada: Primeiro procura-se um campo fly no objeto flyingCar1. Não se encontra. Depois procura-se no protótipo desse objeto. Encontra-se!

Estudemos agora a chamada:

Primeiro procura-se um campo toString no objeto no objeto flyingCar1. Não se encontra. Depois procura-se no protótipo desse objeto. Não se encontra. Depois procura-se no protótipo do protótipo. Encontra-se!


Programar em JavaScript, imitando as classes do Java

Programar em JavaScript usando construtores não é tão simples e agradável como devia ser. Cada vez há mais programadores de JavaScript que procuram outros estilos.

No ECMAScript 6, em 2015, foram introduzidas classes para trabalhar com objetos e herança. Essas classes fazem lembrar as classes do Java. Oferecem sintaxe mais simples e familiar para lidar com os objetos do JavaScript. Infelizmente, por ser muito recente, o novo mecanismo ainda só está a começar a ser implantado.

Nesta cadeira, por diversas razões, também seguimos pelo mesmo caminho e usamos classes. Mas definimos a nossa própria implementação. Introduzimos classes e criamos os nossos objetos através duma primitiva NEW. Introduzimos também a primitiva EXTENDS para definir herança entre classes e uma operação SUPER para aceder à versão original dos métodos redefinidos.

Para programar neste estilo, precisamos de copiar a classe JSRoot (com a sua operação SUPER), mais as funções NEW e EXTENDS, para o início dos nossos programas. Não é preciso perceber os detalhes de implementação, embora seja instrutivo estudá-los: envolvem a introdução dum construtor vazio e a alteração direta do seu protótipo.

Usamos as palavras NEW, EXTENDS e SUPER em maiúsculas porque as correspondentes palavras em minúsculas estão reservadas no JavaScript.

A função NEW aplica-se a uma classe. Cria um objeto e inicializa-o usando o método de inicialização INIT que está definido na classe.

Precisamos de distinguir entre dois tipos de objetos:

Ao programar neste estilo, consideramos apenas membros públicos.

Os dois exemplos que se seguem ilustram esta forma de programar em JavaScript. Veja como o código fica parecido com Java, em termos de conceitos e de estrutura, embora não em termos de sintaxe.

Exemplo 1

Hierarquia de classes para representar pontos a uma dimensão, duas dimensões e três dimensões. Fatoriza-se o código ao máximo, inclusivamente usando a construção SUPER sempre que for aplicável.

Exemplo 2

Representação de expressões algébricas, onde pode ocorrer uma variável "x".

Método hasOwnProperty

Se estivermos na dúvida se uma dada propriedade está presente num objeto de forma direta ou indireta através de herança, podemos confirmar isso usando este método. Exemplo:

Propriedades privadas em objetos

Quando se escreve um objeto literal, esse objeto fica com todas as suas propriedades públicas. Nos objetos literais, não há suporte para propriedades privadas. No entanto, conseguimos obter um efeito equivalente usando funções anónimas com variáveis locais. Essas variáveis locais vão funcionar como campos privados dum objeto.

Estude o seguinte exemplo:

O seguinte objeto literal tem todos os campos públicos:

Para tornar privado o campo 'y', usa-se o esquema abaixo. Note que "this.y" foi reescrito "y".

Explicação: Escreve-se uma função anónima com uma variável local chamada 'y'. No contexto dessa função define-se um objeto, o qual pode usar a variável 'y'. A função está preparada para retornar esse objeto quando for chamada. A função é chamada imediatamente e assim o objeto fica disponível no exterior; mas a variável 'y' permanece privada e só pode ser acedida a partir dos métodos do objeto.



#60