Percursos
/
JavaScript
JavaScript
/
Programa
/
Protótipos e classes
Pr

Protótipos e classes em JavaScript

32 exercícios

Sobre Protótipos e classes

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 que são funções chamam-se métodos.

O JavaScript não tinha classes antes de estas serem adicionadas à especificação da linguagem em 2015, mas permitia programação orientada a objetos com herança baseada em protótipos. E, embora hoje exista a palavra-chave class, o JavaScript continua a ser uma linguagem baseada em protótipos.

Para perceber o que significa ser uma linguagem baseada em protótipos e como o JavaScript funciona na realidade, vamos recuar à época em que não existiam classes.

Sintaxe de protótipos

Função construtora

Em JavaScript, o modelo (classe) é assegurado 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 por letra maiúscula. As instâncias (objetos) derivam do modelo através da 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 interna oculta, referida como [[prototype]] na especificação da linguagem. Guarda uma referência ao valor da chave prototype da função construtora. Sim, leste bem: uma função JavaScript pode ter pares chave/valor, porque também é um objeto nos bastidores.

Desde 2015, é possível aceder a [[prototype]] através de Object.getPrototypeOf(). Antes disso, era acessível através da chave __proto__ em muitos ambientes.

Não confundas o protótipo de um objeto ([[prototype]]) com a propriedade prototype da função construtora.

Note

Para resumir:

  • Os construtores em JavaScript são funções normais.
  • Construir uma nova instância cria um objeto com uma relação com o seu construtor, chamada 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) torna-se 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 valores iniciais quando o objeto é construído. Isto pode ser feito através da palavra-chave this. Dentro da função construtora, this representa o novo objeto que será criado através de new. O this é devolvido automaticamente pela função construtora quando esta é chamada com new.

Isso significa que podemos adicionar campos à nova instância, adicionando-os ao 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 devido à 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 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 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 em questão existe diretamente no próprio objeto. Se não existir, continua a procurar a chave no objeto referenciado pela propriedade [[prototype]] do objeto original. Como já referimos, no nosso exemplo o [[prototype]] aponta para a propriedade prototype da função construtora. É aí que o JavaScript encontra a função startEngine, porque foi aí 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 JavaScript, por exemplo toString(). O [[prototype]] de Object é normalmente null, pelo que a cadeia de protótipos termina aí. Concluindo, 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. Encontras um exemplo detalhado no artigo da MDN «Inheritance and the prototype chain».

Caution

Repara que a cadeia de protótipos só é percorrida quando se obtém um valor. Definir ou eliminar diretamente 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.

Métodos dinâmicos (adicionar métodos a todas as instâncias existentes)

O JavaScript permite-te adicionar métodos a todas as instâncias existentes, mesmo depois de estas terem sido criadas.

Vimos que cada instância mantém uma referência à propriedade prototype da função construtora. Isso significa que, se adicionares uma entrada a esse objeto prototype, a nova entrada (por exemplo, um novo método) fica imediatamente disponível para todas as instâncias criadas a partir dessa função construtora.

function Car() {
  this.engineRunning = false;
}

const myCar = new Car();
// Calling myCar.startEngine() here would result in "TypeError:
// myCar.startEngine is not a function".

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

myCar.startEngine();
// This works, even though myCar was created before the method
// was added.

Em teoria, os métodos dinâmicos podem até ser usados para estender a funcionalidade de objetos incorporados como Object ou Array, modificando o seu protótipo. A isto chama-se monkey patching. Como esta alteração afeta toda a aplicação, deve ser evitada para prevenir efeitos secundários indesejados. O único caso de uso razoável é fornecer um polyfill para um método em falta em ambientes mais antigos.

Sintaxe de classes

Hoje em dia, o JavaScript permite definir classes com a palavra-chave class. Isto foi adicionado à especificação da linguagem em 2015. Por um lado, isto oferece 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 adaptam-se com mais facilidade. 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, seguida 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 contém també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 Objects.

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

Tal como acontece com as declarações de funções e as expressões de funções, o JavaScript também suporta expressões de classes, além da declaração de classe mostrada acima.

Tem em conta que, nos bastidores, o JavaScript continua a ser uma linguagem baseada em protótipos. Todos os mecanismos que vimos 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 e atribuir-lhes valores diretamente.

A introdução de campos privados a sério na especificação da linguagem está em curso; consulta o documento da proposta para mais detalhes.

Entretanto, podes seguir a convenção estabelecida de que os campos e métodos que começam por um underscore devem ser tratados como privados. Nunca devem ser acedidos diretamente a partir do exterior 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 se lhe atribui 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.

Campos e métodos de classe

Na POO, às vezes queres fornecer campos ou métodos utilitários que não dependem da instância específica. Em vez disso, são definidos para a própria classe. Isto consegue-se com a palavra-chave static.

class Car {
  static type = 'vehicle';

  static isType(targetType) {
    return targetType === 'vehicle';
  }
}

Car.type;
// => 'vehicle'

Car.isType('road sign');
// => false

Herança baseada em classes

Além do tipo de herança ao longo da cadeia de protótipos que vimos antes, também podes representar herança entre classes em JavaScript. Isto é abordado no conceito Inheritance.


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

Editar via GitHub A ligação abre numa nova janela ou separador

Aprende Protótipos e classes