Existem algumas opções...
A mais simples na minha opinião são closures:
function createPerson(name) {
const Person = {
getName: function(){
return name
},
setName: function(newName){
name = newName
}
}
return Person
}
const joao = createPerson('João da Silva')
// joao não tem propriedade name
// você está criando ela agora, mas não importa,
// os métodos getters e setters não acessam essa propriedade
joao.name = 'Jose'
console.log(joao.getName()) // 'João da Silva'
console.log(joao)
//{
// getName: //...
// name: "Jose",
// setName: //...
//}
joao.setName('Ricardo')
console.log(joao.getName()) // 'Ricardo'
const maria = createPerson('Maria de Souza')
maria.name = 'Marinalva'
console.log(maria.getName())
console.log(maria)
A função retorna um objeto, este objeto não tem a propriedade nome, na verdade foi criada uma closure. Você não consegue de fora alterar o nome.
Um impacto disso é que você não se beneficia dos protótipos, simplesmente cria um novo objeto que tem aqueles métodos. Já com protótipos (classes em js usam protótipos) um objeto acessa o método de outro, ou seja, você não tem várias cópias daquele método. Protótipos te ajudam a poupar memória.
O padrão acima se chama factory function.
Também é possível utilizar a closure na classe, mas você teria que criar seus métodos dentro do construtor, o que daria na mesma que usar factory functions (cada nova instância teria sua própria cópia dos métodos), você pode ver um exemplo nesta resposta do StackOverflow.
Há uma maneira para se introduzir privacidade nas classes utilizando WeakMaps, ainda não utilizei, não tenho certeza absoluta se é possível acessar de fora, mas a princípio parece promissor, você pode dar uma olhada aqui. O artigo do link anterior também menciona uma maneira utilizando o novo tipo do JavaScript Symbol()
porém existe uma maneira de acessar.