Como o JavaScript funciona: os componentes internos de classes e herança + transpilação no Babel e no TypeScript

Robisson Oliveira
React Brasil
10 min readJul 3, 2019

--

Este é o post #15 da série dedicada a explorar o JavaScript e seus componentes de construção. No processo de identificação e descrição dos elementos centrais, também compartilhamos algumas regras práticas que usamos ao criar o SessionStack , um aplicativo JavaScript que precisa ser robusto e altamente eficiente para ajudar os usuários a ver e reproduzir seus defeitos do aplicativo da Web em tempo real.

Abaixo estão os posts dessa série publicados até o momento

A maneira mais popular de estruturar qualquer tipo de projeto de software hoje em dia é usando classes. Nesta postagem 15, vamos explorar diferentes maneiras de implementar classes em JavaScript e como podemos construir hierarquias de classes. Começaremos mergulhando em como os protótipos funcionam e analisando maneiras de simular a herança baseada em classes em bibliotecas populares. A seguir, veremos como o transpiling pode adicionar recursos à linguagem que não são suportados nativamente e como foi usado no Babel e no TypeScript para introduzir o suporte das classes do ECMAScript 2015. Por último, terminaremos com alguns exemplos de como as classes são implementadas nativamente no V8.

Visão geral

Em JavaScript, não existem tipos primitivos e tudo o que criamos é um objeto. Por exemplo, se criarmos uma nova string:

Podemos chamar imediatamente métodos diferentes no objeto recém-criado:

Ao contrário de outras linguagens, em JavaScript, a declaração de uma string ou de um número cria automaticamente um objeto que encapsula o valor e fornece diferentes métodos que podem ser executados até mesmo nos tipos primitivos.

Outro fato interessante é que tipos complexos, como arrays, também são objetos. Se você observar o tipo de uma instância de matriz, verá que é um objeto. O índice de cada elemento na lista é apenas propriedade no objeto. Então, quando você acessa um elemento pelo seu índice na matriz, você acessa uma propriedade do objeto da matriz e obtém o valor da última. Quando se trata da maneira como os dados são armazenados, essas duas definições são idênticas:

Como resultado, o tempo que leva para acessar um elemento na matriz e uma propriedade de um objeto é o mesmo. Eu descobri isso da maneira mais difícil. Algum tempo atrás eu tive que fazer uma otimização massiva em uma parte crítica do código em um projeto. Depois de tentar todas as opções fáceis, substituí todos os objetos que foram usados ​​no projeto por matrizes. Em teoria, acessar um elemento em uma matriz é mais rápido do que acessar uma chave em um mapa de hash. Fiquei surpreso ao descobrir que isso não teve nenhum efeito sobre o desempenho. Em JavaScript, ambas as operações são implementadas como acessar uma chave em um mapa de hash e levar o mesmo tempo.

Simulando classes com protótipos

Quando pensamos em objetos, a primeira coisa que vem à mente são as classes. Estamos todos acostumados a estruturar nossos aplicativos em termos de classes e as relações entre eles. Embora objetos em JavaScript estejam em toda parte, a linguagem não usa a herança clássica baseada em classes. Em vez disso, ele se baseia em protótipos .

Em JavaScript, todo objeto está conectado a outro objeto — seu protótipo. Quando você tenta acessar uma propriedade ou um método no objeto, a pesquisa é executada primeiro no próprio objeto. Se nada for encontrado, a pesquisa continua no protótipo do objeto.

Começaremos com um exemplo simples que define um construtor para nossa classe base:

Anexamos a função render ao protótipo porque queremos que cada instância da classe Component possa localizá-lo. Quando você chama esse método em qualquer instância da classe Component, primeiro uma pesquisa será executada na própria instância. Em seguida, uma pesquisa será executada no protótipo e é onde o método de renderização será encontrado.

Então, agora vamos tentar estender a classe de componentes. Nós vamos apresentar uma nova classe filha.

Se quisermos que o InputField estenda a funcionalidade da classe de componentes e consiga chamar seu método de renderização, precisamos alterar seu protótipo. Quando um método é chamado em uma instância da classe filha, não queremos pesquisar em seu protótipo vazio. A pesquisa deve continuar na classe Component.

Dessa forma, o método render pode ser encontrado no protótipo da classe Component. Para obter herança, precisamos conectar o protótipo do InputField a uma instância da classe Component. A maioria das bibliotecas usa o método Object.setPrototypeOf para fazer isso.

Isso, no entanto, não é a única coisa que precisamos fazer. Toda vez que estendemos uma classe, precisamos:

  • Defina o protótipo da classe filha como uma instância da classe pai.
  • Chame o construtor pai no construtor filho para que a lógica de inicialização no construtor pai possa ser executada.
  • Introduza uma maneira de acessar um método pai. Você precisa disso quando sobrescreve um método e deseja chamar a implementação original no método pai.

Como você pode ver, se quiser obter todos os recursos da herança baseada em classes, você precisa executar essa lógica complexa toda vez. Sempre que você precisar criar muitas classes, faz sentido encapsular a lógica em funções reutilizáveis. Foi assim que os desenvolvedores originalmente resolveram o problema de ter uma herança baseada em classes — simulando-a com bibliotecas diferentes. Essas soluções se tornaram muito populares, o que tornou óbvio que algo estava faltando na linguagem. É por isso que uma nova sintaxe para criar classes que suportam a herança baseada em classes foi introduzida com a primeira grande revisão da linguagem ECMAScript 2015.

Classes de transpilação

Quando os novos recursos do ES6 ou do ECMAScript 2015 foram propostos, a comunidade de desenvolvedores do JavaScript não pôde esperar por todos os mecanismos e navegadores para começar a apoiá-los. Uma boa maneira de conseguir isso foi através do transpiling. Ele permite que uma parte do código que foi escrita no ECMAScript 2015 seja transformada em JavaScript que qualquer navegador possa entender. Isso inclui a capacidade de escrever classes com herança baseada em classes e tê-las transpiladas para o código de trabalho.

Um dos transpilers mais populares para JavaScript é o Babel. Vamos ver como o transpiling funciona, executando-o em uma definição de classe para a classe de componentes que exploramos acima:

É assim que Babel transpila a definição de classe:

Como você pode ver, o código é transformado em ECMAScript 5, que pode ser executado em qualquer ambiente. Além disso, algumas funções são adicionadas. Eles fazem parte da biblioteca padrão de Babel.

O _classCallCheck e o _createClass são incluídos como funções no arquivo compilado. O primeiro garante que a função construtora nunca seja chamada como uma função. Isso é obtido verificando se o contexto no qual a função é avaliada é uma instância do objeto Component. O código verifica se isso aponta para essa instância. A segunda função _createClass manipula a criação das propriedades do objeto que são passadas como uma lista de objetos com uma chave e um valor.

Para explorar como funciona a herança, vamos analisar a classe InputField que herda de Component.

Aqui está a saída que obtemos quando processamos o exemplo acima usando Babel.

Neste exemplo, a lógica de herança é encapsulada na chamada de função _inherits. Ele executa as mesmas ações descritas na seção anterior, definindo o protótipo da classe filha como uma instância da classe pai.

Para transpilar o código, o Babel realiza várias transformações. Primeiro, o código ECMAScript 2015 é analisado e transformado em uma representação intermediária chamada de árvore de sintaxe abstrata, que já discutimos em uma postagem anterior . Então esta árvore é transformada em uma árvore de sintaxe abstrata diferente, onde cada nó é transformado em seu equivalente ECMAScript 5. Finalmente, o AST é transformado em código.

Abstract Syntax Tree em Babel

A AST contém nós, cada um dos quais possui apenas um nó pai. No Babel, existe um tipo de base para os nós. Ele contém informações sobre o que é o nó e onde ele pode ser encontrado no código. Existem diferentes tipos de nós, como Literals, que representam string, números, nulls, etc. Existem também nós de instruções para controle de fluxo (if) e loops (para, while). E também há um tipo especial de nó para classes. É um filho da classe Node base. Ele estende isso adicionando campos para armazenar referências à classe base e ao corpo da classe como um nó separado.

Vamos transformar o trecho de código a seguir em uma Árvore de Sintaxe Abstrata:

Veja como a Abstract Syntax Tree para este snippet se parece:

Depois que a Árvore de Sintaxe Abstrata é criada, cada nó é transformado em seu nó ECMAScript 5 equivalente e de volta ao código que segue o padrão ECMAScript 5. Isso é feito por um processo que localiza os nós mais distantes do nó raiz e os transforma em código. Em seguida, seus nós pai são transformados em código usando os snippets que já são gerados para cada filho e assim por diante. Esse processo é chamado de travessia em profundidade .

No exemplo acima, primeiro será gerado o código para os dois nós MethodDefinition, seguido pelo código do nó do corpo da classe e, finalmente, pelo código do nó ClassDeclaration.

Transpilação com o TypeScript

Outro framework popular que aproveita o transpiling é o TypeScript. Ele apresenta uma nova sintaxe para gravar aplicativos JavaScript que são transformados no EMCAScript 5 que qualquer navegador ou mecanismo pode executar. Aqui está como podemos implementar a classe de componentes com o Typescript:

E aqui está a AST:

Também suporta herança.

Aqui está o que o resultado do transpiling é:

O resultado final é novamente o código ECMAScript 5 com algumas funções da biblioteca TypeScript. A lógica que é encapsulada em __extends é a mesma que discutimos na primeira seção.

Com o Babel e o TypeScript sendo amplamente adotados, as classes padrão e a herança baseada em classes se tornam o modo padrão de estruturar aplicativos JavaScript. Isso empurrou para a introdução de suporte nativo para classes nos navegadores.

Suporte nativo

Em 2014, o suporte nativo para classes foi introduzido no Chrome. Isso permite que a sintaxe da declaração de classe seja executada sem a necessidade de quaisquer bibliotecas ou transpiladores.

O processo de implementação nativa de classes é o que chamamos de sintaxe de açúcar. Essa é apenas uma sintaxe sofisticada que compila os mesmos primitivos que já são suportados na linguagem. Você pode usar a nova definição de classe fácil de usar, mas ela ainda levará à criação de construtores e à atribuição de protótipos.

Suporte V8

Vamos ver como o suporte nativo para as classes do ECMAScript 2015 funciona no V8. Como discutimos no artigo anterior , primeiro a nova sintaxe deve ser analisada como código JavaScript válido e adicionada à AST. Então, como resultado da definição de classe, um novo nó com o tipo ClassLiteral é adicionado à árvore.

Este nó armazena algumas coisas. Primeiro, ele mantém o construtor como uma função separada. Ele também contém uma lista de propriedades de classe. Eles podem ser um método, um getter, um setter, um campo público ou um campo privado. Esse nó também armazena uma referência à classe pai que esta classe estende, que armazena novamente o construtor, a lista de propriedades e a classe pai.

Uma vez que este novo ClassLiteral é transformado em código , ele é traduzido novamente em funções e protótipos.

Para nós da SessionStack , otimizar cada parte do nosso código tem sido um trabalho muito importante, mas também muito desafiador. Existem duas razões para o alto nível de otimizações necessárias em nosso final.

A primeira é a nossa biblioteca, que é integrada a aplicativos da web — está coletando dados de sessões de usuários, como eventos de usuários, alterações de DOM, dados de rede, exceções, mensagens de depuração e assim por diante. Capturar esses dados sem causar impacto no desempenho foi um desafio que resolvemos com sucesso.

A segunda razão para ficar obcecado com as otimizações é o nosso jogador, que tem que recriar como um vídeo tudo o que aconteceu com os usuários finais no momento em que tiveram um problema ao navegar em um aplicativo da web. O player é altamente otimizado para renderizar com precisão e fazer uso de todos os dados coletados para oferecer uma simulação de pixel perfeito do navegador dos usuários finais e tudo que aconteceu nele, tanto do ponto de vista visual quanto técnico. Fazemos tudo isso em um ambiente de sandbox, em tempo real e diretamente no navegador, o que requer uma excelente utilização do loop de eventos para criar uma experiência agradável e tranquila.

Existe um plano gratuito se você quiser experimentar o SessionStack.

Este é um artigo traduzido com a autorização do autor. O artigo original pode ser lido em https://blog.sessionstack.com/how-javascript-works-the-internals-of-classes-and-inheritance-transpiling-in-babel-and-113612cdc220

Autor do post original — Alexander Zlatkov — Co-founder & CEO @SessionStack

--

--

Robisson Oliveira
React Brasil

Senior Cloud Application Architect at Amazon Web Services(AWS). My personal reflections on software development