Linguagens e Ambientes de Programação (2022/2023)



Teórica 21 (18/mai/2023)

Programação orientada pelos objetos em JavaScript.
Programação o-o baseada em protótipos.
Programação o-o baseada em classes.



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 o-o baseada em protótipos

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

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 objetos 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 Node.js), 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 o-o 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!


Programação o-o usando classes

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 familiar para lidar com os objetos do JavaScript. Estão disponíveis as mesmas palavras reservadas do Java, com significados absolutamente idênticos: class, extends, this, super, static, new e instanceof.

Atenção que neste contexto o termo construtor passa a designar um conceito um pouco diferente, parecido com o conceito homónimo do Java.

Usando estas classes é possível programar em JavaScript usando as abordagens habituais do Java e uma sintaxe bastante parecida. Mesmo assim, é importante listar as diferenças mais importantes:

As classes do JavaScript são apenas açúcar sintático, porque internamente usam-se os objetos e os protótipos originais do JavaScript.

Também em JavaScript, faz sentido em falar em classes concretas e classes abstratas.

Os dois exemplos que se seguem ilustram a forma de programar em JavaScript usando classes, imitando fielmente o estilo habitual do Java.

Exemplo 1

Nós estamos interessados em problemas com hierarquias de classes e onde surja o desafio de fatorizar código ao máximo. Código fatorizado está muitas vezes associado a conceitos abstratos, mas há exceções, como neste exemplo.

Eis uma 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". Exemplo: -(2*x*x+5*(x+5)+3).

A representação natural usa uma árvore com nós de tipos variados. A definição em OCaml seria assim, usando um tipo soma com 5 variantes:

Em JavaScript vamos definir 5 classes concretas, mas através de fatorização vamos identificar algumas classes abstratas.

Fatoriza-se o código ao máximo usando classes abstratas e super.

Por exemplo, a classe BinNode, consideramo-la abstrata porque captura a noção abstrata de nó binário e porque não tencionamos criar objetos desse tipo. Algum do código das classes concretas AddNode e MultNode é fatorizado na classe BinNode.



Vídeos antigos

Estes vídeos poderão estar um pouco desatualizados, pois foram feitos no contexto duma edição anterior do LAP. Contudo, partes dos vídeos poderão continuar a ter alguma utilidade.

#---