Protótipos de objetos

Protótipos são o mecanismo pelo qual objetos JavaScript herdam recursos uns dos outros. Neste artigo, explicamos como as cadeias de protótipos funcionam e observamos como a propriedade prototype pode ser usada para adicionar métodos aos construtores existentes.

Pré-requisitos: Entender como funções em JavaScript funcionam, familiaridade com o básico de JavaScript (veja Primeiros Passos e Elementos Construtivos), e o básico de Orientação a Objetos em JavaScript (veja Introdução a Objetos).
Objetivo: Entender protótipos de objetos JavaScript, como a cadeia de protótipos funciona, e como adicionar novos métodos à propriedade prototype.

Uma linguagem baseada em protótipos?

O JavaScript é frequentemente descrito como uma linguagem baseada em protótipos — para fornecer herança, os objetos podem ter um objeto de protótipo, que atua como um objeto de modelo do qual herda métodos e propriedades. O objeto de protótipo de um objeto também pode ter um objeto de protótipo, do qual herda métodos e propriedades, e assim por diante. Isso geralmente é chamado de cadeia de protótipos e explica por que objetos diferentes têm propriedades e métodos definidos em outros objetos disponíveis para eles.

Bem, para ser exato, as propriedades e os métodos são definidos na propriedade prototype nas funções construtoras dos Objetos, não nas próprias instâncias do objeto.

Em JavaScript, é feito um link entre a instância do objeto e seu protótipo (sua propriedade __proto__, que é derivada da propriedade prototype no construtor), e as propriedades e os métodos são encontrados percorrendo a cadeia de protótipos.

Nota: É importante entender que há uma distinção entre o protótipo de um objeto (que está disponível por meio de Object.getPrototypeOf(obj), ou por meio da propriedade __proto__ ) e a propriedade prototype em funções construtoras. O primeiro é a propriedade em cada instância e o último é a propriedade no construtor. Ou seja, Object.getPrototypeOf(new Foobar()) refere-se ao mesmo objeto que Foobar.prototype.

Vejamos um exemplo para tornar isso um pouco mais claro.

Noções básicas sobre objetos de protótipo

Aqui voltaremos ao exemplo em que terminamos de escrever nosso construtor Person() — carregamos o exemplo em seu navegador. Se você ainda não o conseguiu trabalhar no último artigo, use nosso exemplo oojs-class-further-exercises.html (veja também o código-fonte).

Neste exemplo, definimos uma função construtora, assim:

js
function Person(first, last, age, gender, interests) {
  // property and method definitions
  this.first = first;
  this.last = last;
  //...
}

Nós criamos então uma instância de objeto como esta:

js
var person1 = new Person("Bob", "Smith", 32, "male", ["music", "skiing"]);

Se você digitar "person1." em seu console JavaScript, você deve ver o navegador tentar concluir automaticamente isso com os nomes de membros disponíveis neste objeto:

Nesta lista, você verá os membros definidos no construtor de person1's constructor — Person()name, age, gender, interests, bio, e greeting. No entanto, você também verá alguns outros membros — watch, valueOf, etc — estes estão definidos no objeto de protótipo do Person(), que é Object.

O que acontece se você chamar um método em person1, que é realmente definido em Object? Por exemplo:

js
person1.valueOf();

Este método — Object.valueOf() é herdado por person1 porque seu construtor é Person(), e o protótipo de Person() é Object(). valueOf() retorna o valor do objeto em que é chamado — experimente e veja! Nesse caso, o que acontece é:

  • O navegador verifica inicialmente se o objeto person1 tem um método valueOf() disponível nele, conforme definido em seu construtor, Person().
  • Se não tem, então o navegador verifica se o objeto (Object()) de protótipo do construtor Person() tem um método valueOf() disponível, então ele é chamado, e tudo está bem!

Nota: Queremos reiterar que os métodos e as propriedades não são copiados de um objeto para outro na cadeia de protótipos — eles são acessados ao percorrer a cadeia como descrito acima.

Nota: Não existe uma maneira oficial de acessar diretamente o objeto protótipo de um objeto — os "links" entre os itens da cadeia são definidos em uma propriedade interna, chamada de [[prototype]] na especificação da linguagem JavaScript (veja ECMAScript (en-US)). A maioria dos navegadores modernos, no entanto, tem uma propriedade disponível neles chamada __proto__ (que é sublinhada em ambos os lados), que contém o objeto de protótipo do construtor do objeto. Por exemplo, tente person1.__proto__ and person1.__proto__.__proto__ para ver como a cadeia se parece no código!

Desde ECMAScript 2015 você pode acessar o objeto protótipo de um objeto indiretamente via Object.getPrototypeOf(obj).

A propriedade prototype: Onde os membros herdados são definidos

Então, onde estão as propriedades e os métodos herdados definidos? Se você observar a página de referência do Object, verá, à esquerda, um grande número de propriedades e métodos — muito mais do que o número de membros herdados que vimos disponíveis no objeto person1. Alguns são herdados e outros não — por que isso acontece?

Como mencionado acima, os herdados são os definidos na propriedade prototype (você poderia chamá-lo de um subespaço de nomes) — ou seja, aqueles que começam com Object.prototype, e não os que começam com apenas Object. O valor da propriedade prototype é um objeto, que é basicamente um bucket para armazenar propriedades e métodos que queremos que sejam herdados por objetos mais abaixo na cadeia de protótipos.

Portanto, Object.prototype.watch(), Object.prototype.valueOf(), etc., estão disponíveis para qualquer tipo de objeto que herda de Object.prototype, incluindo novas instâncias de objeto criadas a partir do construtor Person().

Object.is(), Object.keys(), e outros membros não definidos dentro do bloco prototype não são herdados por instâncias de objetos ou tipos de objetos que herdam de Object.prototype. Eles são métodos / propriedades disponíveis apenas no próprio construtor Object().

Nota: Isso parece estranho — como você pode ter um método definido em um construtor, que é em si uma função? Bem, uma função também é um tipo de objeto — veja a referência do construtor Function() se você não acredita em nós.

  1. Você pode conferir as propriedades de protótipo existentes para si mesmo — volte ao nosso exemplo anterior e tente inserir o seguinte no console JavaScript:
    js
    Person.prototype;
    
  2. A saída não mostrará muito porque não definimos nada no protótipo do nosso construtor personalizado! Por padrão, o prototype de um construtor sempre começa vazio. Agora tente o seguinte:
    js
    Object.prototype;
    

Você verá um grande número de métodos definidos na propriedade prototype do Object, que estão disponíveis em objetos que herdam Object, como mostrado anteriormente.

Você verá outros exemplos de herança de cadeia de protótipos em todo o JavaScript — tente procurar os métodos e propriedades definidos no protótipo dos objetos globais String, Date, Number, e Array, por exemplo. Estes todos têm um número de membros definidos em seu protótipo, e é por isso que, por exemplo, quando você cria uma string, como esta:

js
var myString = "This is my string.";

myString imediatamente tem vários métodos úteis disponíveis, como split(), indexOf(), replace(), etc.

Nota: Vale a pena ler nosso guia mais aprofundado sobre Como usar protótipos em JavaScript, uma vez que você tenha entendido esta seção e queira saber mais. Esta seção é intencionalmente simplificada para tornar esses conceitos um pouco mais fáceis de entender quando você os conhecer pela primeira vez.

Aviso: Importante: A propriedade prototype é uma das partes com o nome mais confuso do JavaScript — você pode pensar que this aponta para o objeto de protótipo do objeto atual, mas não (esse é um objeto interno que pode ser acessado por __proto__, lembra?) . Em vez disso, prototype é uma propriedade que contém um objeto no qual você define os membros que deseja herdar.

Revisitando create()

Anteriormente mostramos como o método Object.create() pode ser usado para criar uma nova instância de objeto.

  1. Por exemplo, tente isso no console JavaScript do seu exemplo anterior:
    js
    var person2 = Object.create(person1);
    
  2. O que create() realmente faz é criar um novo objeto a partir de um objeto de protótipo especificado. Aqui, person2 está sendo criado usando person1 como um objeto de protótipo. Você pode verificar isso inserindo o seguinte no console:
    js
    person2.__proto__;
    

Isso retornará o person1.

A propriedade do construtor

Toda função de construtor possui uma propriedade prototype cujo valor é um objeto que contém uma propriedade constructor. Esta propriedade construtora aponta para a função construtora original. Como você verá na próxima seção, as propriedades definidas na propriedade Person.prototype (ou, em geral, na propriedade prototype de uma função construtora, que é um objeto, conforme mencionado na seção acima) tornam-se disponíveis para todos os objetos de instância criados usando Construtor Person(). Portanto, a propriedade constructor também está disponível para os objetos person1 e person2.

  1. Por exemplo, tente estes comandos no console:
    js
    person1.constructor;
    person2.constructor;
    
    Estes devem retornar o construtor Person(), pois contém a definição original dessas instâncias. Um truque inteligente é que você pode colocar parênteses no final da propriedade do constructor (contendo quaisquer parâmetros necessários) para criar outra instância de objeto daquele construtor. O construtor é uma função depois de tudo, então pode ser chamado usando parênteses; você só precisa incluir a palavra-chave new para especificar que deseja usar a função como um construtor.
  2. Tente isso no console:
    js
    var person3 = new person1.constructor("Karen", "Stephenson", 26, "female", [
      "playing drums",
      "mountain climbing",
    ]);
    
  3. Agora tente acessar os recursos do seu novo objeto, por exemplo:
    js
    person3.name.first;
    person3.age;
    person3.bio();
    

Isso funciona bem. Você não precisará usá-lo com frequência, mas pode ser realmente útil quando você deseja criar uma nova instância e não tem uma referência ao construtor original facilmente disponível por algum motivo.

A propriedade do constructor tem outros usos. Por exemplo, se você tiver uma instância de objeto e desejar retornar o nome do construtor do qual ela é uma instância, use o seguinte:

js
instanceName.constructor.name;

Tente isso, por exemplo:

js
person1.constructor.name;

Nota: O valor de constructor.name pode mudar (devido à herança prototípica, ligação, pré-processadores, transpilers, etc.), portanto, para exemplos mais complexos, você desejará usar o operador instanceof.

Modificando Protótipos

Vamos dar uma olhada em um exemplo de modificação da propriedade prototype de uma função construtora — os métodos adicionados ao protótipo estão então disponíveis em todas as instâncias de objeto criadas a partir do construtor. Neste ponto, finalmente adicionaremos algo ao protótipo do nosso construtor Person().

  1. Volte para o nosso exemplo de oojs-class-further-exercises.html e faça uma cópia local do código-fonte. Abaixo do JavaScript existente, adicione o seguinte código, que adiciona um novo método à propriedade prototype do construtor:
    js
    Person.prototype.farewell = function () {
      alert(this.name.first + " has left the building. Bye for now!");
    };
    
  2. Salve o código e carregue a página no navegador e tente inserir o seguinte na entrada de texto:
    js
    person1.farewell();
    

Você deve receber uma mensagem de alerta, mostrando o nome da pessoa, conforme definido dentro do construtor. Isso é realmente útil, mas o que é ainda mais útil é que toda a cadeia de herança foi atualizada dinamicamente, disponibilizando automaticamente esse novo método em todas as instâncias de objeto derivadas do construtor.

Pense nisso por um momento. Em nosso código, definimos o construtor, então criamos um objeto de instância a partir do construtor, então adicionamos um novo método ao protótipo do construtor:

js
function Person(first, last, age, gender, interests) {
  // property and method definitions
}

var person1 = new Person("Tammi", "Smith", 32, "neutral", [
  "music",
  "skiing",
  "kickboxing",
]);

Person.prototype.farewell = function () {
  alert(this.name.first + " has left the building. Bye for now!");
};

Mas o método farewell() ainda está disponível na instância do objeto person1 — seus membros foram atualizados automaticamente para incluir o método farewell().

Nota: Se você está tendo problemas para fazer este exemplo funcionar, dê uma olhada no nosso exemplo oojs-class-prototype.html (veja também running live).

Você raramente verá propriedades definidas na propriedade prototype, porque elas não são muito flexíveis quando definidas dessa forma. Por exemplo, você poderia adicionar uma propriedade assim:

js
Person.prototype.fullName = "Bob Smith";

Isso não é muito flexível, pois a pessoa pode não ser chamada assim. Seria muito melhor construir o fullName fora do name.first e name.last:

js
Person.prototype.fullName = this.name.first + " " + this.name.last;

No entanto, isso não funciona, pois this fará referência ao escopo global nesse caso, não ao escopo da função. Chamar essa propriedade retornaria undefined undefined. Isso funcionou bem no método que definimos anteriormente no protótipo porque ele está dentro um escopo de função, que será transferido com sucesso para o escopo da instância do objeto, portanto, você pode definir propriedades constantes no protótipo (ou seja, aquelas que nunca precisam ser alteradas), mas geralmente funciona melhor definir propriedades dentro do construtor.

Na verdade, um padrão bastante comum para mais definições de objetos é definir as propriedades dentro do construtor e os métodos no protótipo. Isso torna o código mais fácil de ler, pois o construtor contém apenas as definições de propriedade e os métodos são divididos em blocos separados. Por exemplo:

js
// Constructor with property definitions

function Test(a, b, c, d) {
  // property definitions
}

// First method definition

Test.prototype.x = function() { ... };

// Second method definition

Test.prototype.y = function() { ... };

// etc.

Esse padrão pode ser visto em ação no exemplo de aplicativo de plano escolar de Piotr Zalewa.

Sumário

Este artigo abrangeu protótipos de objetos JavaScript, incluindo como cadeias de objetos de protótipos permitem que objetos herdem recursos uns dos outros, a propriedade prototype e como ela pode ser usada para adicionar métodos a construtores e outros tópicos relacionados.

No próximo artigo, veremos como você pode implementar a herança de funcionalidade entre dois dos seus próprios objetos personalizados.