Trilhas
/
JavaScript
JavaScript
/
Programa
/
Protótipos & classes
Pr

Protótipos & classes em JavaScript

32 exercícios

Sobre Protótipos & classes

O JavaScript inclui recursos para programação orientada a objetos (POO). Em 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 de 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 havia classes.

Sintaxe de protótipos

Função construtora

No JavaScript, o modelo (classe) é criado por meio de uma função comum. Quando uma função deve ser usada como esse tipo de 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 observar 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 interna oculta chamada [[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 em JavaScript pode ter pares de chave/valor porque, nos bastidores, ela também é um objeto.

Desde 2015, é possível acessar [[prototype]] por meio de Object.getPrototypeOf(). Antes disso, era possível acessá-lo pela chave __proto__ em muitos ambientes.

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

Note

Para resumir:

  • Construtores em JavaScript são funções comuns.
  • Construir uma nova instância cria um objeto com uma relação com seu construtor, chamada de seu protótipo.
  • Funções são objetos (objetos que podem ser chamados) e, portanto, podem ter propriedades.
  • A propriedade prototype do construtor (da função) se tornará 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 valores iniciais sejam passados para eles quando o objeto é construído. Isso pode ser feito por meio da palavra-chave this. Dentro da função construtora, this representa o novo objeto que será criado por meio de new. this é retornado automaticamente da 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 por meio de this. Isso funciona por causa da seguinte regra geral.

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

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 é chamado de 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 puder, 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 JavaScript, por exemplo, toString(). O [[prototype]] de Object geralmente é null, então a cadeia de protótipos termina aí. Em conclusão, você pode chamar myCar.toString() e esse método existirá porque o JavaScript procura por esse método em toda a cadeia de protótipos. Você pode encontrar um exemplo detalhado no artigo da MDN "Inheritance and the prototype chain".

Caution

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

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

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

Aprendemos que toda instância mantém uma referência à propriedade prototype da função construtora. Isso significa que, se você adicionar uma entrada àquele objeto prototype, essa nova entrada (por exemplo, um novo método) fica imediatamente disponível para todas as instâncias criadas a partir daquela 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.

Na teoria, métodos dinâmicos podem até ser usados para estender a funcionalidade de objetos nativos como Object ou Array, modificando o protótipo deles. Isso é chamado de monkey patching. Como essa mudança afeta toda a aplicação, ela deve ser evitada para prevenir efeitos colaterais indesejados. O único caso de uso razoável é fornecer um polyfill para um método ausente em ambientes mais antigos.

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 fornece 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. Quem vem dessas linguagens se adapta com mais facilidade. 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 classes

Com a nova sintaxe, as classes são definidas com a palavra-chave class, seguida do nome da classe e do 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 dos 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

Assim como as declarações de função e as expressões de função, o JavaScript também dá suporte a expressões de classe, além da declaração de classe mostrada acima.

Tenha em mente que, nos bastidores, 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 em JavaScript. É possível acessá-los e atribuir valores a eles diretamente.

Adicionar campos privados de verdade à especificação da linguagem está em andamento; veja o documento da proposta para mais detalhes.

Enquanto isso, você pode usar a 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 por 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 recebe uma atribuição.

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 de classe e métodos de classe

Em POO, às vezes você quer fornecer campos ou métodos utilitários que não dependem da instância específica. Em vez disso, eles são definidos para a própria classe. Isso pode ser feito 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, você também pode representar herança entre classes em JavaScript. Isso é abordado em Conceito Herança.


  1. Exemplos de this - Como um método de objeto, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (referenciado em 3 de dezembro de 2021) ↩

Editar via GitHub O link abre em uma nova janela ou aba

Aprenda Protótipos & classes