Percursos
/
JavaScript
JavaScript
/
Exercícios
/
Sistema de janelas
Sistema de janelas

Sistema de janelas

Exercício de aprendizagem

Introdução

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.

Sintaxe de protótipos

Função construtora

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.

Note

Para resumir:

  • Os construtores em JavaScript são funções normais.
  • A construção de uma nova instância cria um objeto com uma relação com o seu construtor, a que se chama o seu protótipo.
  • As funções são objetos (objetos que podem ser chamados) e, por isso, podem ter propriedades.
  • A propriedade prototype do construtor (da função) passa a ser o protótipo da instância.

Campos de 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

Métodos de instância

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

A cadeia de protótipos

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.

Caution

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.

Sintaxe de 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.

Declarações de classes

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.

Campos privados, getters e setters

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.

  1. 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) ↩

Instruções

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.

1. Define Size para armazenar as dimensões da janela

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

2. Define Position para armazenar a posição de uma janela

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

3. Define uma classe ProgramWindow

Define uma classe ProgramWindow com os seguintes campos:

  • screenSize: contém um valor fixo do tipo Size com width 800 e height 600
  • size : 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.

4. Adiciona um método para redimensionar a janela

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.

  • A altura ou largura mínima permitida é 1. Alturas ou larguras pedidas inferiores a 1 serão limitadas a 1.
  • A altura e a largura máximas dependem da posição atual da janela; as bordas da janela não podem ultrapassar as bordas do ecrã. Os valores superiores a estes limites serão limitados ao maior tamanho que podem assumir. Por exemplo, se a posição da janela estiver em 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

5. Adiciona um método para mover a janela

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.

  • A posição mínima é 0, tanto para x como para y.
  • A posição máxima em qualquer direção depende do tamanho atual da janela. As bordas não podem ultrapassar as bordas do ecrã. Os valores superiores a estes limites serão limitados ao valor máximo que podem assumir. Por exemplo, se o tamanho da janela estiver em 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

6. Altera uma janela de programa

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.
Editar via GitHub A ligação abre numa nova janela ou separador
JavaScript Exercism

Estás pronto para começar Sistema de janelas?

Inscreve-te no Exercism para aprenderes e dominares JavaScript com 37 conceitos159 exercícios, e mentoria humana real, tudo grátis.