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

Sistema de janelas

Exercício de aprendizagem

Introdução

O JavaScript inclui os recursos para programação orientada a objetos (POO). Na POO, você quer criar objetos (instâncias) a partir de "modelos" (classes) para que eles incluam certos dados e funcionalidades. As propriedades de dados são chamadas de campos no contexto da POO, e as propriedades de função são chamadas de métodos.

O JavaScript não tinha classes antes de elas serem adicionadas à especificação da linguagem em 2015, mas permitia programação orientada a objetos usando herança baseada em protótipos. E mesmo que a palavra-chave class esteja disponível hoje em dia, o JavaScript ainda é uma linguagem baseada em protótipos.

Para entender o que significa ser uma linguagem baseada em protótipos e como o JavaScript realmente funciona, vamos voltar ao tempo em que não existiam classes.

Sintaxe de protótipos

Função construtora

No JavaScript, o modelo (classe) é implementado com uma função comum. Quando uma função deve ser usada como esse modelo, ela é chamada de função construtora, e a convenção é que o nome da função comece com uma letra maiúscula. As instâncias (objetos) são derivadas do modelo usando a palavra-chave new ao invocar a função construtora.

function Car() {
  // ...
}

const myCar = new Car();
const yourCar = new Car();

É importante notar que, no JavaScript, as instâncias e a função construtora mantêm uma relação entre si mesmo depois que as instâncias foram criadas. Todo objeto de instância inclui uma propriedade oculta e interna chamada de [[prototype]] na especificação da linguagem. Ela guarda uma referência ao valor da chave prototype da função construtora. Sim, você leu certo: uma função do JavaScript pode ter pares de chave/valor porque, por trás dos panos, ela também é um objeto.

Note

Para resumir:

  • Construtores no JavaScript são funções comuns.
  • Construir uma nova instância cria um objeto com uma relação com seu construtor, chamada de protótipo.
  • Funções são objetos (objetos chamáveis) e, portanto, podem ter propriedades.
  • A propriedade prototype do construtor (da função) se torna o protótipo da instância.

Campos de instância

Muitas vezes, você quer que todos os objetos derivados (instâncias) incluam alguns campos e que alguns valores iniciais sejam passados para eles quando o objeto é construído. Isso pode ser feito com a palavra-chave this. Dentro da função construtora, this representa o novo objeto que será criado via new. this é retornado automaticamente pela função construtora quando ela é chamada com new.

Isso 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 por meio da propriedade prototype da função construtora. Dentro de um método, você pode acessar os campos da instância via this. Isso funciona por causa da seguinte regra geral.

Quando uma função é chamada como método de um objeto, seu this recebe o objeto no qual o método foi 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

myCar no exemplo acima é um objeto comum do JavaScript e, se o inspecionássemos (por exemplo, no console do navegador), não encontraríamos uma propriedade startEngine com uma função como valor diretamente dentro do objeto myCar. Então como o código acima funciona, afinal?

O segredo aqui é a cadeia de protótipos. Quando você tenta acessar qualquer propriedade (campo ou método) de um objeto, o JavaScript primeiro verifica se a chave correspondente pode ser encontrada diretamente no próprio objeto. Se não, ele continua procurando a chave no objeto referenciado pela propriedade [[prototype]] do objeto original. Como mencionado antes, em nosso exemplo [[prototype]] aponta para a propriedade prototype da função construtora. É aí que o JavaScript encontraria a função startEngine, porque nós a adicionamos lá.

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). Ela contém métodos gerais que estão disponíveis para todos os objetos do JavaScript, como toString(). Concluindo, você pode chamar myCar.toString() e esse método existirá porque o JavaScript procura por ele em toda a cadeia de protótipos.

Caution

Repare que a cadeia de protótipos só é percorrida ao se obter um valor. Definir uma propriedade diretamente ou excluir uma propriedade de um objeto de instância afeta apenas aquela instância específica. Isso pode não ser o que você espera se estiver acostumado com uma linguagem de herança baseada em classes.

Sintaxe de classes

Hoje em dia, o JavaScript permite definir classes com a palavra-chave class. Isso foi adicionado à especificação da linguagem em 2015. Por um lado, isso oferece açúcar sintático que torna as classes mais fáceis de ler e escrever. A nova sintaxe é mais parecida com a forma como as classes são escritas em linguagens como C++ ou Java. Desenvolvedores que vêm dessas linguagens têm mais facilidade para se adaptar. Por outro lado, a sintaxe de classes abre caminho para novos recursos da linguagem que não estão disponíveis na sintaxe de protótipos.

Declarações de classe

Com a nova sintaxe, as classes são definidas com a palavra-chave class, seguidas pelo nome da classe e pelo corpo da classe entre chaves. O corpo contém a definição da função construtora, ou seja, um método especial com o nome constructor. Essa 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 para os métodos é parecida com a notação abreviada que vimos para adicionar funções como valores dentro de um objeto, veja 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

Tenha em mente que, por trás dos panos, o JavaScript ainda é uma linguagem baseada em protótipos. Todos os mecanismos que aprendemos na seção "Sintaxe de protótipos" acima continuam valendo.

Campos privados, getters e setters

Por padrão, todos os campos de instância são públicos no JavaScript. Eles podem ser acessados e atribuídos diretamente.

No entanto, existe uma convenção estabelecida de que campos e métodos que começam com um sublinhado devem ser tratados como privados. Eles nunca devem ser acessados diretamente de fora da classe.

Campos privados às vezes são acompanhados de getters e setters. Com as palavras-chave get e set, você pode definir funções que são executadas quando uma propriedade com o mesmo nome da função é acessada ou atribuída.

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 objeto, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (consultado em 3 de dezembro de 2021) ↩

Instruções

Neste exercício, você vai simular um sistema de computador baseado em janelas. Você vai criar algumas janelas que podem ser movidas e redimensionadas. A imagem a seguir é representativa dos valores com os quais você vai trabalhar abaixo.

                  <--------------------- screenSize.width --------------------->

       ^          ┌────────────────────────────────────────────────────────────┐
       |          │                                                            │
       |          │         position.x, _                                      │
       |          │         position.y   \                                     │
       |          │                       \<----- size.width ----->            │
       |          │                 ^      *──────────────────────┐            │
       |          │                 |      │        title         │            │
       |          │                 |      ├──────────────────────┤            │
screenSize.height │                 |      │                      │            │
       |          │            size.height │                      │            │
       |          │                 |      │       contents       │            │
       |          │                 |      │                      │            │
       |          │                 |      │                      │            │
       |          │                 v      └──────────────────────┘            │
       |          │                                                            │
       |          │                                                            │
       v          └────────────────────────────────────────────────────────────┘

📣 Para praticar sua ampla variedade de habilidades em JavaScript, tente resolver as tarefas 1 e 2 com sintaxe de protótipo e as tarefas restantes com sintaxe de classe.

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

Defina uma classe (função construtora) chamada Size. Ela deve ter dois campos, width e height, que armazenam as dimensões atuais da janela. A função construtora deve aceitar valores iniciais para esses campos. A largura é fornecida como primeiro parâmetro e a altura como segundo. A largura e a altura padrão devem ser 80 e 60, respectivamente.

Além disso, defina um método resize(newWidth, newHeight) que recebe uma nova largura e uma nova 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. Defina Position para armazenar a posição de uma janela

Defina uma classe (função construtora) chamada Position com dois campos, x e y, que armazenam a posição horizontal e vertical atuais, respectivamente, do canto superior esquerdo da janela. A função construtora deve aceitar valores iniciais para esses campos. O valor de x é fornecido como primeiro parâmetro e o valor de y como segundo. O valor padrão deve ser 0 para ambos os campos.

A posição (0, 0) é o canto superior esquerdo da tela, com os valores de x aumentando conforme você se move para a direita e os valores de y aumentando conforme você se move para baixo.

Defina 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. Defina uma classe ProgramWindow

Defina uma classe ProgramWindow com os seguintes campos:

  • screenSize: guarda um valor fixo do tipo Size com width 800 e height 600
  • size: guarda um valor do tipo Size; o valor inicial é o valor padrão da instância de Size
  • position: guarda um valor do tipo Position; o valor inicial é o valor padrão da instância de Position

Quando a janela é aberta (criada), ela sempre tem o tamanho e a posição padrão no início.

const programWindow = new ProgramWindow();
programWindow.screenSize.width;
// => 800

// Similar for the other fields.

Observação: o nome ProgramWindow é usado em vez de Window para diferenciar a classe da classe Window nativa que existe em ambientes de navegador.

4. Adicione um método para redimensionar a janela

A classe ProgramWindow deve incluir um método resize. Ele deve aceitar um parâmetro do tipo Size como entrada e tentar redimensionar a janela para o tamanho especificado.

No entanto, o novo tamanho não pode ultrapassar certos limites.

  • A altura ou largura mínima permitida é 1. Alturas ou larguras solicitadas menores que 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 passar das bordas da tela. Valores maiores que esses 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 solicitado um redimensionamento para height = 400, width = 300, a janela seria redimensionada para height = 300, width = 300, pois a tela não é grande o suficiente na direção y para acomodar totalmente a solicitação.
const programWindow = new ProgramWindow();

const newSize = new Size(600, 400);
programWindow.resize(newSize);
programWindow.size.width;
// => 600
programWindow.size.height;
// => 400

5. Adicione um método para mover a janela

Além da funcionalidade de redimensionamento, a classe ProgramWindow também deve incluir um método move. Ele deve aceitar um parâmetro do tipo Position como entrada. O método move é semelhante ao resize, mas ajusta a posição da janela para o valor solicitado, em vez do tamanho.

Assim como em resize, a nova posição não pode ultrapassar certos limites.

  • A menor posição é 0 tanto para x quanto para y.
  • A posição máxima em qualquer direção depende do tamanho atual da janela. As bordas não podem passar das bordas da tela. Valores maiores que esses limites serão limitados ao maior tamanho que podem assumir. Por exemplo, se o tamanho da janela estiver em x = 250, y = 100 e for solicitada uma movimentação para x = 600, y = 200, a janela seria movida para x = 550, y = 200, pois a tela não é grande o suficiente na direção x para acomodar totalmente a solicitação.
const programWindow = new ProgramWindow();

const newPosition = new Position(50, 100);
programWindow.move(newPosition);
programWindow.position.x;
// => 50
programWindow.position.y;
// => 100

6. Altere uma janela de programa

Implemente uma função changeWindow que aceita uma instância de ProgramWindow como entrada e altera a janela para o tamanho e a posição especificados. A função deve retornar a instância de ProgramWindow que foi passada, depois de aplicadas as alterações.

A janela deve receber uma largura de 400, 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 O link abre em uma nova janela ou aba
JavaScript Exercism

Tudo pronto para começar Sistema de janelas?

Crie sua conta no Exercism para aprender e dominar JavaScript com 37 conceitos159 exercícios e mentoria humana de verdade, tudo de graça.