O problema do underscore
Durante anos, o JavaScript não teve campos privados de verdade. A convenção era colocar um underscore na frente da propriedade e torcer para que todo mundo respeitasse:
_count parece privado, mas não é. Qualquer parte do código pode ler, escrever ou até deletar esse valor. O underscore é só uma plaquinha educada na porta — a porta em si continua escancarada.
O JavaScript moderno resolveu isso com campos privados de verdade, marcados com #.
A sintaxe # torna o campo privado
Basta colocar # antes do nome do campo, tanto na declaração quanto em cada lugar onde ele for acessado:
De dentro da classe, this.#count funciona normalmente. De fora, ele simplesmente não existe:
O # faz parte do nome do campo, literalmente. Não é uma palavra-chave modificadora como o private de outras linguagens — é um símbolo (sigil) que o parser usa para acessar um slot separado e protegido no objeto. É por isso que o erro aparece em tempo de parsing, antes mesmo do código rodar.
Métodos privados e getters com
Campos não são as únicas coisas que você pode marcar como privadas. Métodos, getters e setters também aceitam o prefixo #:
#assertPositive é um helper interno. Ele não faz parte da API pública, então torná-lo realmente privado garante que ninguém vai chamá-lo por acidente de fora — e ninguém consegue depender dele, o que te deixa livre para renomear ou remover depois.
Membros estáticos privados
Membros estáticos também podem ser privados. Basta prefixar com # do mesmo jeito:
Estáticos privados ficam na própria classe, não nas instâncias. Úteis para contadores, caches ou configurações que não devem vazar para fora da classe.
Subclasses não enxergam esses campos
Esse detalhe pega muita gente que vem de Java ou C#. Em JavaScript, os campos privados são privados da classe, e não privados da instância. Ou seja, uma subclasse não consegue acessar os campos privados da classe pai:
Em JavaScript não existe protected. Se uma subclasse precisa acessar os dados, a classe pai tem que expor isso através de um método, getter ou (mais raramente) um campo não privado. Essa decisão é proposital: privado é privado mesmo, e a herança não abre brechas nisso.
Verificando um campo privado com in
Em alguns casos, você quer confirmar se um objeto realmente pertence à sua classe — o famoso brand check. O operador in funciona com nomes de campos privados dentro da classe:
Como só Wallet consegue criar objetos com #balance, testar #balance in obj é uma forma confiável de verificar se obj é realmente uma instância de Wallet. Em alguns casos, é até mais rápido e seguro do que usar instanceof, já que campos privados não podem ser forjados de fora da classe.
Pegadinha comum: objetos literais não têm campos privados
Os campos privados existem apenas nas instâncias criadas pelo construtor da classe. Se você tentar usar um campo privado em um objeto que não foi criado com new, a linguagem lança um erro:
Chamar o método com um this que não seja uma instância de Point lança um erro em tempo de execução. Esse é o mecanismo por trás da verificação de marca mostrada antes: os campos privados ficam amarrados à classe específica que os declarou, e não a qualquer objeto que simplesmente tenha a mesma cara.
Quando usar # em campos privados
Use campos privados por padrão sempre que algum estado ou auxiliar não fizer parte da API pública da classe. Os motivos:
- Liberdade para refatorar. Quem consome a classe não consegue depender de detalhes internos que sequer enxerga.
- Encapsulamento de verdade. Nada de leituras, escritas ou exclusões acidentais vindas de fora.
- Autocomplete mais limpo. Os editores não sugerem membros privados para quem está usando a classe por fora.
Use propriedades públicas quando algo realmente faz parte da interface. Use um getter (get name()) quando quiser acesso somente leitura a um campo privado. E pode esquecer a convenção do underline: ela era uma gambiarra para suprir uma lacuna que a linguagem agora já preencheu.
#celsius é o armazenamento interno, enquanto celsius e fahrenheit são apenas views de leitura. Quem consome a classe não tem como corromper o estado interno, e a própria classe fica livre para mudar a forma como guarda esse valor no futuro.
A seguir: Prototypes
As classes são, em boa parte, açúcar sintático em cima do sistema de prototypes do JavaScript — o modelo mais antigo e fundamental sobre o qual a linguagem foi de fato construída. Entender prototypes deixa claro por que o this se comporta do jeito que se comporta, como a herança funciona de verdade e o que acontece quando uma classe faz extends por baixo dos panos. É o assunto da próxima página.
Perguntas frequentes
Como declarar um campo privado em JavaScript?
Basta colocar # na frente do nome do campo — tanto na declaração quanto em todo acesso. Por exemplo: class Counter { #count = 0; increment() { this.#count++; } } cria um campo de fato privado. Vale lembrar: o # faz parte do nome, não é um operador.
Qual a diferença entre #field e _field em JavaScript?
#field e _field em JavaScript?_field é só convenção de nomenclatura — a propriedade continua pública e qualquer um pode ler ou escrever nela. Já #field é garantido pela própria linguagem: código fora da classe simplesmente não consegue acessar, e tentar gera um SyntaxError já em tempo de parse. Se você quer privacidade de verdade, use #.
Subclasses conseguem acessar campos privados da classe pai?
Não. Private fields têm escopo limitado à classe que os declara — nem mesmo subclasses enxergam. Se a subclasse precisar do valor, a classe pai precisa expor um método ou getter. É uma regra mais rígida que o protected de outras linguagens, e isso é proposital.
Dá para verificar se um objeto tem um determinado campo privado?
Sim, usando o operador in dentro da classe: #field in obj retorna true se obj tiver aquele campo privado. É bastante útil para brand checks — ou seja, confirmar que o objeto é realmente uma instância da sua classe antes de chamar métodos nele.