Dev.to · 7 min read

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

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)); 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}`) } } 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}`) } 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 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 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 Inheritance and the prototype chain Object.getPrototypeOf() Object.prototype.__proto__ Object.create() Classes Prototype Pollution Prevention Cheat Sheet Créditos de imagem Foto de Leo_Visions na Unsplash

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News