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.
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.
Para resumir:
prototype do construtor (da função) torna-se o protótipo da 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
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
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».
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.
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.
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.
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.
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.
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
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.
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) ↩