As aulas são uma das formas mais populares de estruturar projetos de software atualmente. Essa abordagem de programação também é usada em JavaScript. Hoje estamos publicando uma tradução da parte 15 da série de ecossistemas JS. Este artigo discutirá várias abordagens para implementar classes em JavaScript, mecanismos de herança e transpiração. Começaremos dizendo como os protótipos funcionam e analisando várias maneiras de simular a herança baseada em classes em bibliotecas populares. Em seguida, falaremos sobre como, graças à transpilação, é possível escrever programas JS que usam recursos que não estão disponíveis no idioma ou, embora existam na forma de novos padrões ou propostas que estão em diferentes estágios de aprovação, ainda não foram implementados no JS- motores. Em particular, falaremos sobre as classes Babel, TypeScript e ECMAScript 2015. Depois disso, examinaremos alguns exemplos que demonstram os recursos da implementação interna de classes no mecanismo V8 JS.

[Leitura recomendada] As outras 19 partes do ciclo Revisão
Em JavaScript, somos constantemente confrontados com objetos, mesmo quando parece que estamos trabalhando com tipos de dados primitivos. Por exemplo, crie uma string literal:
const name = "SessionStack";
Depois disso, podemos chamar imediatamente o
name
para chamar vários métodos de um objeto do tipo
String
, para o qual a string literal que criamos será convertida automaticamente.
console.log(name.repeat(2)); // SessionStackSessionStack console.log(name.toLowerCase()); // sessionstack
Diferentemente de outros idiomas, no JavaScript, depois de criar uma variável que contém, por exemplo, uma string ou um número, podemos, sem realizar uma conversão explícita, trabalhar com essa variável como se ela tivesse sido originalmente criada usando a
new
palavra-chave e o construtor correspondente. Como resultado, devido à criação automática de objetos que encapsulam valores primitivos, é possível trabalhar com esses valores como se fossem objetos, em particular, referindo-se a seus métodos e propriedades.
Outro fato digno de nota em relação ao sistema de tipos JavaScript é que, por exemplo, matrizes também são objetos. Se você observar a saída do comando
typeof
para a matriz, poderá ver que ele relata que a entidade sob investigação possui o tipo de dados do
object
. Como resultado, verifica-se que os índices dos elementos da matriz são apenas propriedades de um objeto específico. Portanto, quando acessamos um elemento de uma matriz por índice, tudo se resume a trabalhar com uma propriedade de um objeto do tipo
Array
e obter o valor dessa propriedade. Se falamos sobre como os dados são armazenados dentro de objetos e matrizes comuns, as duas construções a seguir levam à criação de estruturas de dados quase idênticas:
let names = ["SessionStack"]; let names = { "0": "SessionStack", "length": 1 }
Como resultado, o acesso aos elementos da matriz e às propriedades do objeto é realizado na mesma velocidade. O autor deste artigo diz que descobriu durante a solução de um problema complexo. Ou seja, uma vez que ele precisava realizar uma otimização séria de um trecho de código muito importante no projeto. Depois de tentar muitas abordagens simples, ele decidiu substituir todos os objetos usados neste código por matrizes. Em teoria, acessar elementos de matriz é mais rápido do que trabalhar com chaves de tabela de hash. Para sua surpresa, essa substituição não afetou o desempenho de nenhuma maneira, já que trabalhar com matrizes e objetos com JavaScript se resume a interagir com chaves de tabela de hash, o que, em ambos os casos, requer a mesma quantidade de tempo.
Simulando classes usando protótipos
Quando pensamos em objetos, a primeira coisa que vem à mente são as classes. Talvez cada um dos que estão envolvidos na programação de hoje tenha criado aplicativos cuja estrutura se baseia em classes e nos relacionamentos entre eles. Embora objetos em JavaScript possam ser encontrados literalmente em qualquer lugar, a linguagem não usa um sistema tradicional de herança baseado em classe. JavaScript usa
protótipos para resolver problemas semelhantes.
Objeto e seu protótipoNo JavaScript, cada objeto é associado a outro objeto - com seu próprio protótipo. Quando você tenta acessar uma propriedade ou método de um objeto, a pesquisa do que você precisa é realizada primeiro no próprio objeto. Se a pesquisa não tiver êxito, ela continua no protótipo do objeto.
Considere um exemplo simples que descreve uma função de construtor para a classe base
Component
:
function Component(content) { this.content = content; } Component.prototype.render = function() { console.log(this.content); }
Aqui, atribuímos a função
render()
ao método prototype, pois precisamos de cada instância da classe
Component
para usar esse método. Quando, em qualquer instância do
Component
, o método
render
é chamado, sua pesquisa começa no próprio objeto para o qual é chamado. Em seguida, a pesquisa continua no protótipo, onde o sistema encontra esse método.
Protótipo e duas instâncias da classe ComponentAgora vamos tentar estender a classe
Component
. Vamos criar um construtor para uma nova classe -
InputField
:
function InputField(value) { this.content = `<input type="text" value="${value}" />`; }
Se precisamos que a classe
InputField
estenda a funcionalidade da classe
Component
e consigamos chamar seu método de
render
, precisamos alterar seu protótipo. Quando um método é chamado em uma instância de uma classe filho, não faz sentido procurá-lo em um protótipo vazio. Precisamos, na busca por esse método, ser encontrado na classe
Component
. Portanto, precisamos fazer o seguinte:
InputField.prototype = Object.create(new Component());
Agora, ao trabalhar com uma instância da classe
InputField
e chamar o método da classe
Component
, esse método será encontrado no protótipo da classe
Component
. Para implementar o sistema de herança, você precisa conectar o protótipo
InputField
a uma instância da classe
Component
. Muitas bibliotecas usam
Object.setPrototypeOf () para resolver esse problema.
Estendendo a classe Component com a classe InputFieldNo entanto, as ações acima não são suficientes para implementar um mecanismo semelhante à herança tradicional. Cada vez que estendemos a classe, precisamos executar as seguintes ações:
- Faça do protótipo da classe descendente uma instância da classe pai.
- Chame, no construtor da classe descendente, o construtor da classe pai para garantir que a classe pai seja inicializada corretamente.
- Forneça um mecanismo para chamar métodos da classe pai em situações em que a classe descendente substitua o método pai, mas é necessário chamar a implementação original desse método da classe pai.
Como você pode ver, se um desenvolvedor de JS quiser usar os recursos de herança baseada em classe, ele precisará constantemente executar as etapas acima. No caso de você precisar criar muitas classes, tudo isso pode ser feito na forma de funções adequadas para reutilização.
De fato, a tarefa de organizar a herança com base em classes foi inicialmente resolvida na prática do desenvolvimento de JS dessa maneira. Em particular, usando várias bibliotecas. Essas soluções se tornaram muito populares, o que indicava claramente que algo estava claramente ausente no JavaScript. É por isso que o ECMAScript 2015 introduziu novas construções sintáticas destinadas a apoiar o trabalho com classes e a implementar os mecanismos de herança correspondentes.
Transpilação de classe
Após a proposta dos novos recursos do ECMAScript 2015 (ES6), a comunidade JS quis aproveitá-los o mais rápido possível, sem aguardar a conclusão do longo processo de adição de suporte para esses recursos nos mecanismos e navegadores JS. Ao resolver esses problemas, a transpilação é boa. Nesse caso, a compilação é reduzida à transformação do código JS gravado de acordo com as regras do ES6 para uma visualização compreensível para navegadores que até agora não suportam recursos do ES6. Como resultado, por exemplo, torna-se possível declarar classes e implementar mecanismos de herança baseados em classes de acordo com as regras ES6 e converter essas construções em código que funciona em qualquer navegador. Esquematicamente, esse processo, usando o exemplo de processamento de uma função de seta por um transpiler (outro novo recurso de idioma que precisa de tempo para suportar), pode ser representado como mostrado na figura abaixo.
TranspilaçãoUm dos transpilers JavaScript mais populares é o Babel.js. Vamos ver como ele funciona executando uma compilação do código de declaração da classe
Component
, sobre o qual falamos acima. Então, aqui está o código ES6:
class Component { constructor(content) { this.content = content; } render() { console.log(this.content) } } const component = new Component('SessionStack'); component.render();
E aqui está o que esse código se transforma após a transpilação:
var Component = function () { function Component(content) { _classCallCheck(this, Component); this.content = content; } _createClass(Component, [{ key: 'render', value: function render() { console.log(this.content); } }]); return Component; }();
Como você pode ver, o código ECMAScript 5 é obtido na saída do transpiler, que pode ser executado em qualquer ambiente. Além disso, chamadas para algumas funções que fazem parte da biblioteca padrão Babel são adicionadas aqui.
Estamos falando das
_classCallCheck()
e
_createClass()
incluídas no código transpilado. A primeira função,
_classCallCheck()
, foi projetada para impedir que a função construtora seja chamada como uma função regular. Para fazer isso, ele verifica se o contexto no qual a função é chamada é o contexto da instância da classe
Component
. O código verifica se a palavra-chave this aponta para uma instância semelhante. A segunda função,
_createClass()
, cria propriedades do objeto que são passadas a ele como uma matriz de objetos que contêm chaves e seus valores.
Para entender como a herança funciona, analisamos a classe
InputField
, que é a descendente da classe
Component
. Veja como as relações de classe se reúnem no ES6:
class InputField extends Component { constructor(value) { const content = `<input type="text" value="${value}" />`; super(content); } }
Aqui está o resultado da transpilação desse código usando Babel:
var InputField = function (_Component) { _inherits(InputField, _Component); function InputField(value) { _classCallCheck(this, InputField); var content = '<input type="text" value="' + value + '" />'; return _possibleConstructorReturn(this, (InputField.__proto__ || Object.getPrototypeOf(InputField)).call(this, content)); } return InputField; }(Component);
Neste exemplo, a lógica dos mecanismos de herança é encapsulada em uma chamada para a função
_inherits()
. Ele executa as mesmas ações que descrevemos acima, associadas, em particular, à gravação no protótipo da classe descendente de uma instância da classe pai.
Para transpor o código, Babel realiza várias de suas transformações. Primeiro, o código ES6 é analisado e convertido em uma representação intermediária denominada
árvore de sintaxe abstrata . Em seguida, a árvore de sintaxe abstrata resultante é convertida em outra árvore, cada nó transformado em seu equivalente ES5. Como resultado, essa árvore é convertida em código JS.
Árvore de sintaxe abstrata em Babel
Uma árvore de sintaxe abstrata contém nós, cada um dos quais possui apenas um nó pai. Babel tem um tipo de base para nós. Ele contém informações sobre o que é o nó e onde ele pode ser encontrado no código. Existem vários tipos de nós, por exemplo, nós para representar literais, como cadeias, números, valores
null
e assim por diante. Além disso, existem nós para representar expressões usadas para controlar o fluxo de execução do programa (
if
construido) e nós para loops (
for
while
). Há também um tipo especial de nó para representar classes. É um descendente da classe base do
Node
. Ele estende essa classe adicionando campos para armazenar referências à classe base e ao corpo da classe como um nó separado.
Converta o seguinte fragmento de código em uma árvore de sintaxe abstrata:
class Component { constructor(content) { this.content = content; } render() { console.log(this.content) } }
Aqui está como sua representação esquemática será.
Árvore de sintaxe abstrataApós criar uma árvore, cada um de seus nós é transformado em seu nó ES5 correspondente, após o qual essa nova árvore é convertida em código compatível com o padrão ECMAScript 5. Durante o processo de conversão, localize primeiro o nó localizado mais distante do nó raiz, após o qual este nó é convertido em código usando trechos gerados para cada nó. Depois disso, o processo é repetido. Essa técnica é chamada de
pesquisa profunda .
No exemplo acima, o código para os dois nós
MethodDefinition
será gerado primeiro, após o qual o código para o nó
ClassBody
será gerado e, finalmente, o código para o nó
ClassDeclaration
.
Transpilação TypeScript
Outro sistema popular que usa transpilação é o TypeScript. Essa é uma linguagem de programação cujo código é transformado em código ECMAScript 5 que é compreensível para qualquer mecanismo JS. Oferece nova sintaxe para escrever aplicativos JS. Veja como implementar a classe
Component
no TypeScript:
class Component { content: string; constructor(content: string) { this.content = content; } render() { console.log(this.content) } }
Aqui está a árvore de sintaxe abstrata para esse código.
Árvore de sintaxe abstrataTypeScript suporta herança.
class InputField extends Component { constructor(value: string) { const content = `<input type="text" value="${value}" />`; super(content); } }
Aqui está o resultado da transpilação deste código:
var InputField = (function (_super) { __extends(InputField, _super); function InputField(value) { var _this = this; var content = "<input type=\"text\" value=\"" + value + "\" />"; _this = _super.call(this, content) || this; return _this; } return InputField; }(Component));
Como você pode ver, este é novamente um código ES5, no qual, além das construções padrão, há chamadas para algumas funções da biblioteca TypeScript. Os recursos da função
__extends()
semelhantes aos de que falamos no começo deste material.
Graças à ampla adoção do Babel e do TypeScript, os mecanismos para declarar classes e organizar a herança baseada em classes tornaram-se ferramentas padrão para estruturar aplicativos JS. Isso contribuiu para a adição de suporte para esses mecanismos nos navegadores.
Suporte à classe do navegador
O suporte à classe apareceu no navegador Chrome em 2014. Isso permite que o navegador trabalhe com declarações de classe sem o uso de transpilação ou quaisquer bibliotecas auxiliares.
Trabalhando com classes no console do Chrome JSDe fato, o suporte do navegador a esses mecanismos não passa de açúcar sintático. Essas construções são convertidas nas mesmas estruturas básicas que já são suportadas pelo idioma. Como resultado, mesmo se você usar a nova sintaxe, em um nível inferior, tudo parecerá como criar construtores e manipular protótipos de objetos:
Suporte de classe é açúcar sintáticoSuporte de classe na V8
Vamos falar sobre como o suporte à classe ES6 funciona no mecanismo V8 JS. No
material anterior dedicado às árvores de sintaxe abstrata, falamos sobre o fato de que, ao preparar o código JS para execução, o sistema o analisa e forma uma árvore de sintaxe abstrata em sua base. Ao analisar construções de declarações de classe, os nós do tipo
ClassLiteral caem na árvore de sintaxe abstrata.
Esses nós armazenam algumas coisas interessantes. Primeiro, é um construtor como uma função separada e, segundo, é uma lista de propriedades de classe. Podem ser métodos, getters, setters, campos públicos ou privados. Além disso, esse nó armazena uma referência à classe pai, que estende a classe para a qual o nó é formado, que novamente armazena o construtor, a lista de propriedades e um link para sua própria classe pai.
Depois que o novo nó
ClassLiteral
transformado em código , ele é convertido em construções que consistem em funções e protótipos.
Sumário
O autor deste material diz que o
SessionStack se esforça para otimizar o máximo possível o código de sua biblioteca, pois precisa resolver tarefas difíceis de coletar informações sobre tudo o que acontece nas páginas da web. Durante a solução desses problemas, a biblioteca não deve desacelerar o trabalho da página analisada. A otimização desse nível requer levar em conta os menores detalhes do ecossistema JavaScript que afetam o desempenho, em particular, levar em conta os recursos de como as classes e os mecanismos de herança são organizados no ES6.
Caros leitores! Você usa construções de sintaxe ES6 para trabalhar com classes em JavaScript?
