Boa tarde amigos!
Foi lançado o curso
"Segurança dos sistemas de informação" . Em conexão com isso, estamos compartilhando com você a parte final do artigo "Fundamentos dos mecanismos JavaScript: otimização de protótipos", cuja primeira parte pode ser lida
aqui .
Também lembramos que a publicação atual é uma continuação desses dois artigos:
“Noções básicas de mecanismos JavaScript: formulários gerais e cache embutido. Parte 1 " ,
" Noções básicas de mecanismos JavaScript: formulários gerais e cache Inline. Parte 2 " .
Classes e programação de protótiposAgora que sabemos como obter acesso rápido às propriedades dos objetos JavaScript, podemos dar uma olhada na estrutura mais complexa das classes JavaScript. É assim que a sintaxe da classe se parece em JavaScript:
class Bar { constructor(x) { this.x = x; } getX() { return this.x; } }
Embora isso pareça um conceito relativamente novo para JavaScript, é apenas "açúcar sintático" para a programação de protótipo que sempre foi usada em JavaScript:
function Bar(x) { this.x = x; } Bar.prototype.getX = function getX() { return this.x; };
Aqui, atribuímos a propriedade
getX
ao objeto
getX
. Isso funcionará como qualquer outro objeto, pois os protótipos em JavaScript são os mesmos objetos. Nas linguagens de programação de protótipos, como JavaScript, os métodos são acessados por meio de protótipos, enquanto os campos são armazenados em instâncias específicas.
Vamos dar uma olhada no que acontece quando criamos uma nova instância do
Bar
, que chamaremos de
foo
.
const foo = new Bar(true);
Uma instância criada usando esse código possui um formulário com uma única propriedade
'x'
. O protótipo
foo
é
Bar.prototype
, que pertence à classe
Bar
.

Esse
Bar.prototype
tem a forma de si mesmo, contendo a única propriedade
'getX'
, cujo valor é determinado pela função
'getX'
, que quando chamada retorna
this.x
O protótipo
Bar.prototype
é
Object.prototype
, que faz parte da linguagem JavaScript.
Object.prototype
é a raiz da árvore do protótipo, enquanto o seu protótipo é
null
.

Quando você cria uma nova instância da mesma classe, as duas instâncias têm a mesma forma, como já entendemos. Ambas as instâncias
Bar.prototype
para o mesmo objeto
Bar.prototype
.
Acessar propriedades do protótipoBem, agora sabemos o que acontece quando definimos uma classe e criamos uma nova instância. Mas o que acontece se chamarmos o método na instância, como fizemos no exemplo a seguir?
class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX();
Você pode considerar qualquer chamada de método como duas etapas separadas:
const x = foo.getX();
O primeiro passo é carregar o método, que na verdade é uma propriedade do protótipo (cujo valor é uma função). O segundo passo é chamar uma função com uma instância, por exemplo, o valor
this
. Vamos dar uma olhada mais de perto na primeira etapa em que o método
getX
é
getX
partir da instância
foo
.

O mecanismo inicia uma instância de
foo
e percebe que o formulário
foo
não tem
'getX'
, portanto, ele precisa passar pela cadeia de protótipos para encontrá-la. Chegamos ao
Bar.prototype
, olhamos o formulário do protótipo, vemos que ele tem a propriedade
'getX'
com deslocamento zero.
Bar.prototype
o valor desse deslocamento no
Bar.prototype
e encontramos o
JSFunction getX
que estávamos procurando.
A flexibilidade do JavaScript permite que os links em cadeia do protótipo sejam alterados, por exemplo:
const foo = new Bar(true); foo.getX();
Neste exemplo, chamamos
foo.getX()
duas vezes, mas cada vez tem significados e resultados completamente diferentes. É por isso que, apesar de os protótipos serem apenas objetos em JavaScript, acelerar o acesso às propriedades de um protótipo é uma tarefa ainda mais importante para os mecanismos JavaScript do que acelerar o próprio acesso às propriedades de objetos regulares.
Na prática diária, carregar propriedades do protótipo é uma operação bastante comum: isso acontece toda vez que você chama um método!
class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX();
Anteriormente, falamos sobre como os mecanismos otimizam o carregamento de propriedades regulares usando formulários e caches embutidos. Como otimizar o carregamento das propriedades do protótipo para objetos da mesma forma? Acima, vimos como as propriedades são carregadas.

Para fazer isso rapidamente com downloads repetidos nesse caso específico, você precisa conhecer as três coisas a seguir:
- O formulário
foo
não contém 'getX'
e não foi alterado. Isso significa que ninguém alterou o objeto foo adicionando ou removendo uma propriedade ou alterando um dos atributos da propriedade. - O protótipo foo ainda é o protótipo
Bar.prototype
. original. Portanto, ninguém alterou o protótipo foo
usando Object.setPrototypeOf()
ou atribuindo-o à propriedade _proto_
especial. - O formulário
Bar.prototype
contém 'getX'
e não foi alterado. Isso significa que ninguém alterou o Bar.prototype
ao adicionar ou remover uma propriedade ou alterar um dos atributos da propriedade.
No caso geral, isso significa que você precisa fazer uma verificação da própria instância e mais duas verificações para cada protótipo até o protótipo que contém a propriedade desejada. As verificações 1 + 2N, onde N é o número de protótipos usados, não parece tão ruim neste caso, uma vez que a cadeia de protótipos é relativamente rasa. No entanto, os mecanismos geralmente precisam lidar com cadeias de protótipos muito mais longas, como é o caso das classes DOM regulares. Por exemplo:
const anchor = document.createElement('a');
Temos um
HTMLAnchorElement
e chamamos o método
getAttribute()
. A cadeia para este elemento simples já inclui 6 protótipos! A maioria dos métodos DOM que nos interessam não está no
HTMLAnchorElement
protótipo
HTMLAnchorElement
, mas em algum lugar da cadeia.

O método
getAttribute()
está em
Element.prototype
. Isso significa que toda vez que chamamos
anchor.getAttribute()
, o mecanismo JavaScript precisa:
- Verifique se
'getAttribute'
não 'getAttribute'
um objeto anchor
si; - Verifique se o protótipo final é
HTMLAnchorElement.prototype
; - Confirme a ausência de
'getAttribute'
lá; - Verifique se o próximo protótipo é
HTMLElement.prototype
; - Confirme a ausência de
'getAttribute'
; - Verifique se o próximo protótipo é
Element.prototype
; - Verifique se
'getAttribute'
presente nele.
Um total de 7 cheques. Como esse tipo de código é bastante comum na Web, os mecanismos usam vários truques para reduzir o número de verificações necessárias para carregar as propriedades do protótipo.
Voltando a um exemplo anterior, no qual fizemos apenas três verificações ao solicitar
'getX'
para
foo
:
class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const $getX = foo.getX;
Para cada objeto que ocorre antes do protótipo que contém a propriedade desejada, é necessário verificar os formulários quanto à ausência dessa propriedade. Seria bom se pudéssemos reduzir o número de verificações apresentando a verificação do protótipo como uma verificação da ausência de uma propriedade. Em essência, é exatamente isso que os mecanismos fazem com um truque simples: em vez de armazenar o link do protótipo na própria instância, os mecanismos o armazenam na forma.

Cada formulário indica um protótipo. Isso significa que toda vez que o protótipo muda, o mecanismo passa para uma nova forma. Agora, precisamos verificar apenas a forma do objeto para confirmar a ausência de determinadas propriedades, além de proteger o link do protótipo (proteja o link do protótipo).
Com essa abordagem, podemos reduzir o número de verificações necessárias de 2N + 1 para 1 + N para acelerar o acesso. Essa ainda é uma operação bastante cara, pois ainda é uma função linear do número de protótipos na cadeia. Os mecanismos usam vários truques para reduzir ainda mais o número de verificações para um determinado valor constante, especialmente no caso de carregamento sequencial das mesmas propriedades.
Células de validadeO V8 processa formulários de protótipo especificamente para esse fim. Cada protótipo tem uma forma exclusiva que não é compartilhada com outros objetos (em particular, com outros protótipos) e cada uma dessas formas de protótipo possui um
ValidityCell
especial associado a ele.

Este
ValidityCell
desativado sempre que alguém altera o protótipo associado a ele ou a qualquer outro protótipo acima dele. Vamos ver como isso funciona.
Para acelerar downloads de protótipos subsequentes, o V8 coloca o cache Inline em um local de quatro campos:

Quando o cache embutido é aquecido na primeira vez em que o código é executado, o V8 lembra o deslocamento no qual a propriedade foi encontrada no protótipo, esse protótipo (por exemplo,
Bar.prototype
), o formulário da instância (no nosso caso, o formulário
foo
) e também vincula o
ValidityCell
atual ao protótipo recebido da instância do formulário (no nosso caso,
Bar.prototype
é usado).
Na próxima vez que você usar o cache embutido, o mecanismo precisará verificar o formulário da instância e o
ValidityCell
. Se ainda for válido, o mecanismo usará diretamente o deslocamento no protótipo, ignorando as etapas extras de pesquisa.

Quando você altera o protótipo, um novo formulário é realçado e a célula
ValidityCell
anterior é desabilitada. Por esse motivo, o cache Inline é ignorado na próxima vez em que é iniciado, o que leva a um desempenho ruim.
Vamos voltar ao exemplo com o elemento DOM. Cada alteração no
Object.prototype
não apenas invalida os caches Inline para
Object.prototype
, mas também para qualquer protótipo na cadeia abaixo dele, incluindo
EventTarget.prototype
,
Node.prototype
,
Element.prototype
etc., até o próprio
HTMLAnchorElement.prototype
.

De fato, modificar o
Object.prototype
enquanto o código está sendo executado é uma terrível perda de desempenho. Não faça isso!
Vejamos um exemplo específico para entender melhor como isso funciona. Digamos que temos uma classe
Bar
e uma função
loadX
que chama um método em objetos do tipo
Bar
. Chamamos a função
loadX
várias vezes com instâncias da mesma classe.
class Bar { } function loadX(bar) { return bar.getX();
O cache embutido no
loadX
agora aponta para
ValidityCell
for
Bar.prototype
. Se você modificar o
Object.prototype
(mutate), que é a raiz de todos os protótipos no JavaScript, o
ValidityCell
se tornará inválido e os caches Inline existentes não serão usados na próxima vez, resultando em baixo desempenho.
Alterar
Object.prototype
é sempre uma má idéia, pois invalida qualquer cache Inline para protótipos carregados no momento da alteração. Aqui está um exemplo de como NÃO fazer:
Object.prototype.foo = function() { };
Estamos expandindo
Object.prototype
, que invalida todos os caches de protótipo Inline carregados pelo mecanismo neste momento. Em seguida, executaremos algum código que usa o método descrito por nós. O mecanismo precisará iniciar desde o início e configurar caches Inline para qualquer acesso à propriedade prototype. E então, finalmente, "limpe" e remova o método de protótipo que adicionamos anteriormente.
Você acha que limpar é uma boa ideia, certo? Bem, neste caso, vai piorar ainda mais a situação! A remoção das propriedades altera o
Object.prototype
, para que todos os caches inline sejam desabilitados novamente e o mecanismo precise iniciar o trabalho desde o início novamente.
Para resumir . Apesar de protótipos serem apenas objetos, eles são processados especialmente por mecanismos JavaScript, a fim de otimizar o desempenho das pesquisas de métodos por protótipos.
Deixe os protótipos em paz! Ou se você realmente precisar lidar com eles, faça-o antes de executar o código, para não invalidar todas as tentativas de otimizar seu código durante a execução!
Resumir
Aprendemos como o JavaScript armazena objetos e classes, e como formulários, caches embutidos e células de validade ajudam a otimizar as operações dos protótipos. Com base nesse conhecimento, entendemos como melhorar o desempenho de um ponto de vista prático: não toque em protótipos! (ou se você realmente precisar, faça-o antes de executar o código).
←
A primeira parteEsta série de publicações foi útil para você? Escreva nos comentários.