Desenvolvimento

3 abr, 2019

Campos públicos e privados em classes JavaScript – O que vem por aí no ESNext

Publicidade

O JavaScript está em constante evolução através dos comitês responsáveis pela especificação e testes do ECMAScript, como o TC39 e o Test262.

Todos os anos as propostas que chegam ao nível 4 passam a ser incorporadas na nova versão da especificação que será lançada naquele ano.

Em 2018 tivemos o lançamento do ES2018 (que é a 9ª versão, então chamamos de ES9). Neste ano de 2019 devemos ter mais uma atualização da especificação, que estamos chamando de ES10 (ou ES2019).

Descrever todas as novas funcionalidades do JavaScript em 2019 fica para um outro artigo. Neste aqui falaremos do futuro! Isso mesmo, das especificações de 2020 para frente.

Uma das propostas que mais chamou a atenção da comunidade – principalmente pelo fato de ela ter gerado uma grande discussão desde 2017 – são os tão queridos campos públicos e privados nativos! Primeiro vamos entender um pouco sobre como funciona o processo do TC39.

O processo

Existem inúmeros textos sobre esse processo na Internet, então vamos resumir um pouco para as partes importantes.

Você pode ler tudo sobre o processo diretamente na documentação oficial.

O ECMAScript é uma especificação de uma linguagem baseada em scripts. O Javascript segue essa especificação para implementar seus métodos, e não é o único, outras linguagens mais antigas como o ActionScript também se baseavam nessa mesma especificação para construir a base da linguagem.

Quem “desenha” o ECMAScript é o chamado Technical Committee 39, ou TC39 – do qual temos orgulhosamente um brasileiro participando, o grande Leo Balter.

Esse comitê é responsável por discutir e levar as novas propostas da linguagem a serem implementadas de fato.

A ideia é que cada proposta passe por 4 estágios de aprovação baseada em consenso:

  • 1. Proposta: nesse estágio a proposta é formalizada em um documento, e um champion – membro do TC39 – é designado para ser o responsável por ela, já que qualquer pessoa pode fazer uma proposta para a linguagem. Neste documento devem estar as descrições da proposta, a solução via exemplos, modelos de API e algoritmos, juntamente com os possíveis problemas e desafios que podem ser encontrados.
  • 2. Rascunho: após a proposta ser aprovada no estágio 1, ela passa ao status de draft. Neste estágio temos a primeira versão do que vai estar na especificação de fato e a formalização da sintaxe e semântica da funcionalidade deve ser apresentada na linguagem usada para descrever o documento oficial
  • 3. Candidato: no estágio 3 a proposta já está quase finalizada formalmente e precisa de feedbacks de implementações, usuários, comunidades e possíveis problemas que podem acontecer. Pense nisso como sendo um “teste de campo”
  • 4. Finalizada: o último estágio da proposta diz somente que essa proposta está pronta para ser incluída na linguagem. Nesse ponto a proposta já deve ter testes escritos no Test 262 para aceitação e pelo menos duas implementações de produção, que são compatíveis com a especificação passando nestes testes

Mas tome cuidado, propostas podem ser removidas em qualquer estágio – o Object.observe é um exemplo de proposta que foi até o estágio 2 e foi removida, então não é seguro dizermos que qualquer proposta estaria na próxima versão do ECMAScript com certeza.

Apenas propostas no estágio 4 são mais prováveis de entrarem na próxima versão da especificação, mas é mais seguro esperar a confirmação dos editores antes de sair dizendo isso.

Vamos nos ater aqui – se você quiser saber mais, o Dr. Axel Rauschmayer tem um artigo muito bom sobre todo esse processo com mais detalhes.

A proposta

Essa proposta pode ser encontrada no repositório oficial do TC39, juntamente com todos os seus issues e discussões.

Porém, como Mathias Bynens fala em seu artigo sobre ela, essa especificação já está implementada no V8, na versão 7.2 e no Chrome 72 para campos públicos, as campos privados ainda não chegaram.

Basicamente, o que ela diz é que agora podemos ter campos de classes fora dos construtores, como costumamos ter em linguagens como Java e C#. Em um modelo mais simples, o que estamos dizendo é isso:

class Pessoa {
  constructor () {
    this._id = Math.floor(Math.random()*10)
    this._numeroArtigos = 0
  }
  
  get id () {
    return this._id
  }
  
  get artigos () {
    return this._numeroArtigos
  }
  
  incrementarArtigos () {
    this._numeroArtigos++
  }
}

O que está acontecendo aqui é: basicamente estamos criando uma nova classe e incluindo um método chamado incrementarArtigos e dois getters em seu protótipo.

Mas o ponto interessante aqui é que temos um construtor que está criando ambos os valores de _id e _numeroArtigos com valores fixos.

A comunidade adotou como padrão prefixar propriedades com _ quando elas não devem ser acessadas de fora da classe – o equivalente a private _id em qualquer linguagem que suporte encapsulamento – devido ao fato de que o JavaScript não tem nativamente modificadores de acesso para suas propriedades e métodos – estamos apenas confiando que todos vão seguir essa regra comum, mas nada impede de fazermos isso:

const Lucas = new Pessoa()
Lucas._id = 'Olá'
console.log(Lucas.id) // Olá

Ou seja, podemos facilmente bagunçar as propriedades de controle de uma classe, porque isso não é protegido pela linguagem.

Campos públicos

Em linguagens como Java e C# temos uma definição de classe bem concisa com campos e propriedades declaradas diretamente na classe, ao invés de no construtor, por exemplo, em Java:

class Pessoa {
  Random generator = new Random();
  private final int id = generator.nextInt();
  String nome;

  Pessoa (String nome) {
    this.nome = nome;
  }

  public String getId() {
    return id;
  }
}

Assim poderíamos acessar id apenas pelo método da classe instancia.id e nada mais. Se não tivermos a propriedade nome, inclusive, o construtor é completamente desnecessário:

class Pessoa {
  Random generator = new Random();
  private final int id = generator.nextInt();

  public String getId() {
    return id;
  }
}

Esse é o maior ganho dessa proposta. Podermos abdicar do construtor por termos campos definidos diretamente na instancia da classe. Da seguinte forma:

class Pessoa {
  _id = Math.floor(Math.random()*10)
	_numeroArtigos = 0
  
  get id () {
    return this._id
  }
  
  get artigos () {
    return this._numeroArtigos
  }
  
  incrementarArtigos () {
    this._numeroArtigos++
  }
}

Em uma classe pequena como essa não vemos grandes ganhos, mas veremos como essa proposta brilha de verdade usando um componente React através do exemplo que o Tyler Mcginnis deu em seu artigo sobre o assunto:

class PlayerInput extends Component {
  constructor(props) {
    super(props)
    this.state = { username: '' }
    this.handleChange = this.handleChange.bind(this)
  }
  
  handleChange(event) {
    this.setState({ username: event.target.value })
  }
  
  render() {
    ...
  }
}

PlayerInput.propTypes = {
  id: PropTypes.string.isRequired,
  label: PropTypes.string.isRequired,
  onSubmit: PropTypes.func.isRequired,
}

PlayerInput.defaultProps = {
  label: 'Username',
}

Nada de mais, temos um componente clássico do react, vamos passar o state para uma propriedade na instancia usando a nova proposta:

class PlayerInput extends Component {
  state = {
    username: ''
  }

  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
  }

  handleChange(event) {
    this.setState({ username: event.target.value})
  }

  render() {
    ...
  }
}

PlayerInput.propTypes = {
  id: PropTypes.string.isRequired,
  label: PropTypes.string.isRequired,
  onSubmit: PropTypes.func.isRequired,
}

PlayerInput.defaultProps = {
  label: 'Username',
}

Agora note o seguinte: no React nós fazemos a verificação de propriedades fora da classe porque o JS não permite que propriedades estáticas sejam valores, apenas métodos. Então não temos como adicionar propTypes e nem defaultProps dentro da classe usando ES6 porque estamos adicionando valores.

A boa notícia é que isso também é coberto pela nova especificação, ou seja, podemos adicionar também valores estáticos na classe, além de métodos:

class PlayerInput extends Component {
  static propTypes = {
    id: PropTypes.string.isRequired,
    label: PropTypes.string.isRequired,
    onSubmit: PropTypes.func.isRequired,
  }

  static defaultProps = {
    label: 'Username'
  }

  state = {
    username: ''
  }

  constructor(props) {
    super(props)

    this.handleChange = this.handleChange.bind(this)
  }

  handleChange(event) {
    this.setState({
      username: event.target.value
    })
  }

  render() {
    ...
  }
}

Agora não temos mais nenhuma propriedade para fora da nossa classe, mas ainda temos um construtor chamando super com as propriedades. Ele só existe porque temos que executar o bind na nossa função handleChange para que o this (como veremos em um futuro artigo) fique ligado ao contexto de classe correto, que é a classe PlayerInput.

Mas, quando usamos arrow functions não precisamos mais deste bind. Isso porque, como falamos no último artigo sobre escopos, arrow functions tem escopo léxico, ou seja, eles são ligados ao contexto do bloco que estão, ou seja, as coisas devem funcionar normalmente porque nosso this será ligado à classe correta, então podemos simplesmente refatorar nosso método para uma arrow function e nos livrar do construtor:

class PlayerInput extends Component {
  static propTypes = {
    id: PropTypes.string.isRequired,
    label: PropTypes.string.isRequired,
    onSubmit: PropTypes.func.isRequired,
  }

  static defaultProps = {
    label: 'Username'
  }

  state = {
    username: ''
  }

  handleChange = (event) => {
    this.setState({
      username: event.target.value
    })
  }

  render() {
    ...
  }
}

Veja como ficou muito melhor!

Campos privados

Encapsulamento e controle de acesso não estão nas features nativas do JS, portanto, temos que fazer várias gambiarras para poder criar campos privados ou definir propriedades que não podem ser acessadas por fora.

Geralmente fazemos isso brincando com o escopo – se você tem dúvidas, olhe o artigo novamente – para que, para quem está de fora, o escopo não permita a visualização de propriedades. Isso só é possível graças a const e let:

const pessoa = (function () {
  let nomeCompleto = 'Lucas Santos'
  
  return {
    setNome: (novoNome) => {
      if (/\d+/.test(novoNome)) throw new Error('Nome inválido')
      nomeCompleto = novoNome
    },
    getNome() => nomeCompleto
  }
}())

pessoa.getNome() // Lucas Santos
pessoa.setNome('Linus Torvalds')
pessoa.getNome() // Linus Torvalds
pessoa.nomeCompleto = 'Lucas Santos'
pessos.getNome // Linus Torvalds

Veja que ainda assim temos acesso à propriedade. O JS não vai reclamar que acessamos uma propriedade interna porque estamos, na verdade, acessando a propriedade do escopo superior. Essa é uma das formas de simularmos encapsulamento no JS.

Com a proposta de campos privados, ganhamos um novo modificador: o #.

Vale a pena ressaltar este modificador em específico, pois ele gerou muitas discussões (muitas mesmo) sobre se o correto seria implementar o símbolo #.

Alguns consideraram o uso de @, outros perguntaram se não valeria a pena usar private como em outras linguagens e cada vez mais pessoas chegavam sugerindo novas opções, porém o # acabou ganhando.

Esse modificador faz exatamente o que esperamos que ele faça: torna um campo privado. Portanto, não seria possível acessá-lo de fora. Vamos mudar nossa classe Pessoa para explicar – veja que não queremos que ninguém acesse _id ou _numeroArtigos, então vamos torná-las privadas:

class Pessoa {
  #id = Math.floor(Math.random()*10)
	#numeroArtigos = 0
  
  get id () {
    return this.#id
  }
  
  get artigos () {
    return this.#numeroArtigos
  }
  
  incrementarArtigos () {
    this.#numeroArtigos++
  }
}

Agora sempre que alguém quiser acessar o campo de fora, não vai conseguir:

const pessoa = new Pessoa()
pessoa.#id // Syntax Error
pessoa.#id = 30 // Syntax Error

Como já dissemos antes, os campos privados também podem ser estáticos, então podemos fazer algo do tipo:

class Pessoa {
  #id = Math.floor(Math.random()*10)
	static #numeroArtigos = 0
  
  get id () {
    return this.#id
  }
  
  get artigos () {
    return this.#numeroArtigos
  }
  
  incrementarArtigos () {
    Pessoa.#numeroArtigos++
  }
}

Os problemas

Como nem tudo são flores, conseguimos ver alguns problemas do ponto de vista de performance quando usamos campos de classe ao invés de propriedades do construtor.

A primeira delas é que estes campos são injetados diretamente na instância, e não no protótipo da classe, o que faz com que, para cada instância, tenhamos um novo campo ou um novo valor – e isso pode cobrar um preço na memória quando temos muitas instâncias da mesma classe:

class Shape {
  area () {}
}

// É igual a

function Shape () {}
Shape.prototype.area = function () {}

Veja que estamos definindo area uma vez, e como ela está definida no protótipo, seu valor será compartilhado entre todas as instâncias da classe. Agora vamos ver o exemplo com campos de classe:

class Shape {
  area () {}
  perimetro = () => {}
}

// É igual a

function Shape () {
  this.perimetro = function () {}
}
Shape.prototype.area = function () {}

Agora o método perimetro está sendo passado para a classe em si, ou seja, para cada instância dela vamos ter um novo valor de perimetro idêntico. Então é uma boa pensar se vale a pena utilizar campos de classe para definir métodos deste tipo, pois, se muitas instâncias forem criadas, haverá um desperdício de memória.

Conclusão

Como podemos ver, ambas as propostas vão trazer grandes ganhos para todos os programadores JS, principalmente em questão de simplificação e leitura de código.

Essa especificação provavelmente será lançada no ES11 no ano que vem oficialmente, embora muitos browsers, como o Chrome, já estejam implementando algumas dessas propostas (como os campos públicos).

Não vamos esquecer que ela pode ser removida completamente ou parcialmente de acordo com o que dissemos antes, afinal, ela ainda não alcançou o stage 4.

Até mais!