O JavaScript inclui as capacidades para a programação orientada a objetos (POO). Na POO, queres criar objetos (instâncias) a partir de "modelos" (classes) para que incluam determinados dados e funcionalidades. As propriedades de dados chamam-se campos no contexto da POO, e as propriedades de funções chamam-se métodos.
O JavaScript não tinha classes de todo antes de estas serem adicionadas à especificação da linguagem em 2015, mas permitia programação orientada a objetos recorrendo à herança baseada em protótipos.
E, apesar de hoje já existir a palavra-chave class, o JavaScript continua a ser uma linguagem baseada em protótipos.
Para compreender o que significa ser uma linguagem baseada em protótipos e como o JavaScript funciona na realidade, vamos recuar à altura em que não existiam classes.
Em JavaScript, o modelo (a classe) é disponibilizado por uma função normal.
Quando uma função se destina a ser usada como esse modelo, chama-se função construtora e a convenção é que o nome da função comece com letra maiúscula.
As instâncias (objetos) derivam do modelo com a palavra-chave new ao invocar a função construtora.
function Car() {
// ...
}
const myCar = new Car();
const yourCar = new Car();
É importante notar que, em JavaScript, as instâncias e a função construtora mantêm uma relação entre si mesmo depois de as instâncias serem criadas.
Cada objeto instância inclui uma propriedade oculta e interna designada por [[prototype]] na especificação da linguagem.
Esta guarda uma referência ao valor da chave prototype da função construtora.
Sim, leste bem: uma função de JavaScript pode ter pares chave/valor porque, nos bastidores, também é um objeto.
Para resumir:
prototype do construtor (da função) passa a ser o protótipo da instância.Muitas vezes queres que todos os objetos derivados (instâncias) incluam alguns campos e que lhes passes alguns valores iniciais quando o objeto é construído.
Isto pode ser facilitado através da palavra-chave this.
Dentro da função construtora, this representa o novo objeto que será criado através de new.
this é devolvido automaticamente pela função construtora quando esta é chamada com new.
Isto significa que podemos adicionar campos à nova instância adicionando-os a this na função construtora.
function Car(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false
Os métodos são adicionados através da propriedade prototype da função construtora.
Dentro de um método, podes aceder aos campos da instância através de this.
Isto funciona por causa da seguinte regra geral.
Quando uma função é chamada como método de um objeto, o seu
thisé definido como o objeto sobre o qual o método é chamado. 1
function Car() {
this.engineRunning = false;
// ...
}
Car.prototype.startEngine = function () {
this.engineRunning = true;
};
Car.prototype.addGas = function (litre) {
// ...
};
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
O myCar do exemplo acima é um objeto de JavaScript normal e, se o inspecionássemos (por exemplo, na consola do navegador), não encontraríamos uma propriedade startEngine com uma função como valor diretamente dentro do objeto myCar.
Então, como é que o código acima funciona?
O segredo aqui chama-se cadeia de protótipos.
Quando tentas aceder a qualquer propriedade (campo ou método) de um objeto, o JavaScript verifica primeiro se a chave correspondente se encontra diretamente no próprio objeto.
Se não, continua a procurar a chave no objeto referenciado pela propriedade [[prototype]] do objeto original.
Como já foi mencionado, no nosso exemplo [[prototype]] aponta para a propriedade prototype da função construtora.
É aí que o JavaScript encontraria a função startEngine, porque foi lá que a adicionámos.
function Car() {
// ...
}
Car.prototype.startEngine = function () {
// ...
};
E a cadeia não termina aí.
A propriedade [[prototype]] de Car.prototype (myCar.[[prototype]].[[prototype]]) referencia Object.prototype (a propriedade prototype da função construtora Object).
Contém métodos gerais disponíveis para todos os objetos de JavaScript, por exemplo toString().
Em conclusão, podes chamar myCar.toString() e esse método existirá porque o JavaScript procura esse método ao longo de toda a cadeia de protótipos.
Repara que a cadeia de protótipos só é percorrida ao obter um valor. Definir uma propriedade diretamente ou eliminar uma propriedade de um objeto instância afeta apenas essa instância específica. Isto pode não ser o que esperas se estiveres habituado a uma linguagem com herança baseada em classes.
Hoje em dia, o JavaScript suporta a definição de classes com a palavra-chave class.
Isto foi adicionado à especificação da linguagem em 2015.
Por um lado, isto proporciona açúcar sintático que torna as classes mais fáceis de ler e escrever.
A nova sintaxe é mais semelhante à forma como as classes são escritas em linguagens como C++ ou Java.
Os programadores que vêm dessas linguagens têm mais facilidade em adaptar-se.
Por outro lado, a sintaxe de classes abre caminho a novas funcionalidades da linguagem que não estão disponíveis na sintaxe de protótipos.
Com a nova sintaxe, as classes definem-se com a palavra-chave class, seguidas do nome da classe e do corpo da classe entre chavetas.
O corpo contém a definição da função construtora, ou seja, um método especial com o nome constructor.
Esta função funciona exatamente como a função construtora na sintaxe de protótipos.
O corpo da classe também contém todas as definições de métodos.
A sintaxe dos métodos é semelhante à notação abreviada que vimos para adicionar funções como valores dentro de um objeto, vê o conceito Objetos.
class Car {
constructor(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
startEngine() {
this.engineRunning = true;
}
addGas(litre) {
// ...
}
}
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
Tem em mente que, nos bastidores, o JavaScript continua a ser uma linguagem baseada em protótipos. Todos os mecanismos que aprendemos na secção "Sintaxe de protótipos" acima continuam a aplicar-se.
Por predefinição, todos os campos de instância são públicos em JavaScript. É possível aceder-lhes diretamente e atribuir-lhes valores.
Contudo, existe uma convenção estabelecida de que os campos e métodos que começam com um underscore devem ser tratados como privados. Nunca se deve aceder a eles diretamente a partir de fora da classe.
Os campos privados são por vezes acompanhados de getters e setters.
Com as palavras-chave get e set, podes definir funções que são executadas quando se acede a uma propriedade com o mesmo nome da função ou quando lhe é atribuído um valor.
class Car {
constructor() {
this._mileage = 0;
}
get mileage() {
return this._mileage;
}
set mileage(value) {
throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
// Just an example, usually you would not provide a setter in this case.
}
}
const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.
Exemplos de this - Como método de um objeto, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (consultado a 3 de dezembro de 2021) ↩
Neste exercício, vais simular um sistema informático baseado em janelas. Vais criar algumas janelas que podem ser movidas e redimensionadas. A imagem seguinte é representativa dos valores com que vais trabalhar abaixo.
<--------------------- screenSize.width --------------------->
^ ┌────────────────────────────────────────────────────────────┐
| │ │
| │ position.x, _ │
| │ position.y \ │
| │ \<----- size.width -----> │
| │ ^ *──────────────────────┐ │
| │ | │ title │ │
| │ | ├──────────────────────┤ │
screenSize.height │ | │ │ │
| │ size.height │ │ │
| │ | │ contents │ │
| │ | │ │ │
| │ | │ │ │
| │ v └──────────────────────┘ │
| │ │
| │ │
v └────────────────────────────────────────────────────────────┘
📣 Para praticares a tua vasta gama de competências em JavaScript, tenta resolver as tarefas 1 e 2 com sintaxe de protótipos e as tarefas restantes com sintaxe de classes.
Define uma classe (função construtora) chamada Size.
Deve ter dois campos, width e height, que armazenam as dimensões atuais da janela.
A função construtora deve aceitar valores iniciais para estes campos.
A largura é fornecida como primeiro parâmetro e a altura como segundo.
Os valores predefinidos da largura e da altura devem ser 80 e 60, respetivamente.
Além disso, define um método resize(newWidth, newHeight) que recebe uma nova largura e altura como parâmetros e altera os campos para refletir o novo tamanho.
const size = new Size(1080, 764);
size.width;
// => 1080
size.height;
// => 764
size.resize(1920, 1080);
size.width;
// => 1920
size.height;
// => 1080
Define uma classe (função construtora) chamada Position com dois campos, x e y, que armazenam a posição horizontal e vertical atual, respetivamente, do canto superior esquerdo da janela.
A função construtora deve aceitar valores iniciais para estes campos.
O valor de x é fornecido como primeiro parâmetro e o valor de y como segundo.
O valor predefinido deve ser 0 para ambos os campos.
A posição (0, 0) é o canto superior esquerdo do ecrã; os valores de x aumentam à medida que te deslocas para a direita e os valores de y aumentam à medida que te deslocas para baixo.
Define também um método move(newX, newY) que recebe novos parâmetros x e y e altera as propriedades para refletir a nova posição.
const point = new Position();
point.x;
// => 0
point.y;
// => 0
point.move(100, 200);
point.x;
// => 100
point.y;
// => 200
Define uma classe ProgramWindow com os seguintes campos:
screenSize: contém um valor fixo do tipo Size com width 800 e height 600size : contém um valor do tipo Size; o valor inicial é o valor predefinido da instância de Size
position : contém um valor do tipo Position; o valor inicial é o valor predefinido da instância de Position
Quando a janela é aberta (criada), tem sempre o tamanho e a posição predefinidos no início.
const programWindow = new ProgramWindow();
programWindow.screenSize.width;
// => 800
// Similar for the other fields.
Nota à parte: o nome ProgramWindow é usado em vez de Window para distinguir a classe da classe Window incorporada que existe em ambientes de navegador.
A classe ProgramWindow deve incluir um método resize.
Deve aceitar um parâmetro de entrada do tipo Size e tentar redimensionar a janela para o tamanho especificado.
No entanto, o novo tamanho não pode exceder certos limites.
x = 400, y = 300 e for pedido um redimensionamento para height = 400, width = 300, a janela será redimensionada para height = 300, width = 300, uma vez que o ecrã não é suficientemente grande na direção y para acomodar totalmente o pedido.const programWindow = new ProgramWindow();
const newSize = new Size(600, 400);
programWindow.resize(newSize);
programWindow.size.width;
// => 600
programWindow.size.height;
// => 400
Além da funcionalidade de redimensionamento, a classe ProgramWindow também deve incluir um método move.
Deve aceitar um parâmetro de entrada do tipo Position.
O método move é semelhante ao resize; no entanto, este método ajusta a posição da janela para o valor pedido, em vez do tamanho.
Tal como acontece com o resize, a nova posição não pode exceder certos limites.
x como para y.x = 250, y = 100 e for pedida uma deslocação para x = 600, y = 200, a janela será movida para x = 550, y = 200, uma vez que o ecrã não é suficientemente grande na direção x para acomodar totalmente o pedido.const programWindow = new ProgramWindow();
const newPosition = new Position(50, 100);
programWindow.move(newPosition);
programWindow.position.x;
// => 50
programWindow.position.y;
// => 100
Implementa uma função changeWindow que recebe uma instância de ProgramWindow como entrada e altera a janela para o tamanho e a posição especificados.
A função deve devolver a instância de ProgramWindow que foi recebida, depois de as alterações terem sido aplicadas.
A janela deve ficar com uma largura de 400 e uma altura de 300 e ser posicionada em x = 100, y = 150.
const programWindow = new ProgramWindow();
changeWindow(programWindow);
programWindow.size.width;
// => 400
// Similar for the other fields.
Inscreve-te no Exercism para aprenderes e dominares JavaScript com 37 conceitos159 exercícios, e mentoria humana real, tudo grátis.