DEV Community

Cover image for Protótipos: como a herança realmente funciona no JavaScript
Guilherme R.
Guilherme R.

Posted on

Protótipos: como a herança realmente funciona no JavaScript

Introdução

Muitas linguagens como C#, Java, entre outras são descritas como orientadas a objeto, possibilitando o paradigma Programação Orientada a Objeto (POO). No entanto, quando falamos de JS, sabemos que por mais que existam objetos, ela é dita como uma linguagem orientada a protótipos, mas o que de fato isso significa, qual problema isso resolve e como muda a maneira como programamos?

O problema

Tanto a orientação a objeto quanto a orientação a protótipo lidam, entre outras coisas, com a questão de como a herança vai funcionar em determinada linguagem e é justamente nesse ponto que as duas abordagens mais se diferem.

Em linguagens orientadas a objetos as classes de fato existem, contendo propriedades, métodos e servem como molde para a criação de objetos. Com isso, todo objeto criado a partir de uma classe herda suas propriedades e métodos ficando acessíveis para uso.

Como não existem Classes de fato em JavaScript, a herança ocorre de maneira diferente, de objeto para objeto, ligados através da propriedade [[Prototype]] que possui uma referência ao seu protótipo, fazendo com que determinado objeto herde de seu protótipo propriedades e métodos que nunca foram definidos nele.

Exemplo com array

Quando criamos um array, seja de forma literal com [], ou de forma explícita com new Array(), o resultado final é o mesmo: um array cujo [[Prototype]] aponta para o Array.prototype. Essa propriedade .prototype possui um objeto contendo todas as propriedades e métodos que o [[Prototype]] referencia, possibilitando que todos os arrays possam usar métodos como push, pop, map, filter…

Com isso, se irmos além e conferirmos o [[Prototype]] do Array.prototype vamos perceber que ele aponta para o Object.prototype que contém propriedades e métodos também disponível em todo essa cadeia que chamamos de prototype chain.

Por fim, se tentarmos visualizar o protótipo do Object.prototype veremos que é null, pois ele representa o último elo dessa cadeia.

Teste o código abaixo para ver na prática.

const arr = [];

// O código abaixo mostra para qual protótipo o [[Prototype]] de arr aponta,
// exibindo todas as propriedades e métodos do Array.prototype,
// como também, o próprio [[Prototype]] do Array.prototype
// que aponta para o próximo elo da cadeia
console.log(Object.getPrototypeOf(arr));

// O código abaixo mostra para qual protótipo o [[Prototype]] de Array.prototype aponta,
// exibindo todas as propriedades e métodos do Object.prototype,
// sem mostrar o próximo [[Prototype]], visto que Object.prototype
// é o último elo da cadeia, apontando para null
console.log(Object.getPrototypeOf(Array.prototype));

// O código abaixo exibe null
console.log(Object.getPrototypeOf(Object.prototype));
Enter fullscreen mode Exit fullscreen mode

Cadeia de protótipos

Termo O que é
[[Prototype]] Referência interna que liga um objeto ao seu protótipo — não acessível diretamente
__proto__ Forma legada de acessar/definir o [[Prototype]] — prefira Object.getPrototypeOf()
.prototype Propriedade de funções construtoras — contém o molde herdado pelas instâncias

Função construtora X .prototype X Instância

Agora que entendemos o conceito da prototype chain, vamos falar sobre os três papéis que fazem parte dela: a função construtora, o .prototype e a instância.

Funções construtoras

São funções que podem ser tanto invocadas normalmente, como também usadas para criar objetos através do new. Além dos métodos herdados via .prototype, elas também podem ter propriedades e métodos estáticos, declarados diretamente nelas e não dentro do .prototype e por isso acessíveis apenas através da própria função, nunca pelas instâncias criadas a partir dela. É o caso, por exemplo de Object.keys() ou Array.isArray().

.prototype

Toda função construtora possui uma propriedade chamada .prototype, um objeto contendo propriedades e métodos que serão herdados por qualquer instância criada a partir dela. Diferente dos métodos estáticos, o que está dentro do .prototype não pertence a função, mas sim a quem foi criado a partir dela.

Instância

É o objeto criado a partir de uma função construtora, seja de forma explícita (new Array() ou new Object()) ou implícita ([] ou {}). Ela apenas guarda uma referência a esse objeto através do seu [[Prototype]].

É por isso que se um método for adicionado ao .prototype depois que a instância foi criada, ela ainda assim passa a ter acesso a ele, pois não existe cópia e sim uma referência viva e compartilhada.


Aplicações

Entender apenas por entender sem saber como de fato esse conhecimento pode impactar a forma como programamos não faz sentido. Vamos aprender em seguida qual real valor isso traz para o nosso código.

Métodos duplicados e desperdício de memória

Sem entender prototype, pode ser comum definirmos um método da maneira abaixo. Em uma linguagem orientada a objetos isso faria sentido, pois essa definição seria feita na classe, mas lembre-se: JS é orientado a protótipo.

function Person(name) {
    this.name = name;
    this.greet = function() { // Cria uma nova função a cada instância
        console.log(`Oi, ${this.name}`)
    }
}
Enter fullscreen mode Exit fullscreen mode

Cada new Person() cria uma cópia inteira da função greet na memória, ou seja, são funções idênticas ocupando espaço sem necessidade. Isso se torna um problema conforme a escala aumenta. Quem entende .prototype resolve isso colocando o método lá, garantindo que todas as instâncias compartilhem a mesma função.

function Person(name) {
    this.name = name;
}

Person.prototype.greet = function() {
    console.log(`Oi, ${this.name}`)
}
Enter fullscreen mode Exit fullscreen mode

Prototype pollution

Ainda sobre o tópico anterior, agora que sabemos que __proto__ é uma porta de entrada ao [[Prototype]] e que Object.prototype é compartilhado por praticamente todo objeto da aplicação, fica fácil entender que Prototype pollution nada mais é do que uma vulnerabilidade onde dados de uma fonte não confiável como o response de uma requisição acabam sendo escritos diretamente no Object.prototype.

Com isso, como o Object.prototype é compartilhado com praticamente todos os objetos da aplicação, esse problema acaba sendo passado via herança, afetando boa parte do código.

Entender o que class realmente é

Não existe classes em JS, pelo menos não da forma como existe em linguagens como Java ou C#. Em JS a class é apenas uma camada de abstração, que no final das contas nada mais é do que uma função construtora. A falta desse entendimento pode nos levar a pensar que JS funcionará de forma semelhante a Java ou C# ao usar o class.

class Person {
    constructor(name) {
        this.name = name;
    }

    greet() { console.log('Oi'); }
}

typeof Person // function
Person.prototype.greet // O método está aqui
Enter fullscreen mode Exit fullscreen mode

Um detalhe interessante sobre class é que, como ele nos orienta a declarar os métodos fora do constructor, logo não teríamos o problema dos métodos duplicados aqui.

Delegação de comportamento via Object.create()

Quando entendemos que herança em JS nada mais é do que encadear protótipos, notamos que podemos fazer objetos herdarem diretamente de outros objetos, sendo esse método uma alternativa a herança de classes de outras linguagens.

const animal = {
    move() { console.log('movendo'); }
};

const dog = Object.create(animal); // dog herda de animal

dog.move(); // movendo
Enter fullscreen mode Exit fullscreen mode

Conclusão

Sem entender prototype, programamos em JS como se ela fosse uma linguagem de classes. Isso funciona, até termos um problema de performance, bugs ou brechas de segurança. Entender prototype não é sobre saber um detalhe teórico, é sobre programar a favor de como a linguagem realmente funciona.


Referências

Créditos de imagem

Foto de Leo_Visions na Unsplash

Top comments (0)