JS Closures: o que são, e como podem substituir as classes para criar objetos com métodos privados?

Uma alternativa para a criação de objetos utilizando ES2015 (“a nova atualização do Javascript”)


  • “JS não precisa de classes!”
  • “o new é uma desgraça!”
  • “troque suas classes por funções!”.

Se você ja leu alguns artigos sobre JS ou navegou pelas comunidades há grandes chances de ja ter se deparado com algumas dessas frases. Não vou entrar no mérito de discutir se isso está certo ou errado, ao meu ver cada design pattern resolve determinado tipo de problema, e não existe bala de prata. Esse artigo serve pra suprir a dúvida gerada ao ler essas frases: “Mas então, como que eu faço pra não usar classes?”. Closures são uma das suas alternativas para isso.

Todos os exemplos desse artigo são códigos javascript (.js) que podem ser executados no console do seu navegador. Utilizei o Chrome 51.0.2704.103 (64-bit)

Closures

Buscando no MDN temos uma descrição breve do que é uma closure:

Closures (fechamentos) são funções que se referem a variáveis livres (independentes). Em outras palavras, a função definida no closure “lembra” do ambiente em que ela foi criada.

Minha descrição simplificada para closure é: Uma closure é uma função (filha) dentro de outra função (pai). A função interior (filha) pode acessar variáveis da função exterior (pai), mesmo quando a função exterior (pai) não estiver mais sendo executada.

Vamos tentar concretizar essas abstrações com um pouco de código:

Nota: Utilizarei “var” em todos os Gists do artigo pois falar sobre “let” e “const” fugiria do escopo do artigo.

Rode esse bloco de código no seu console do navegador e você verá que a primeira chamada de “pai()” mostra 2 e 1 no log, e a segunda chamada mostra 300 e 150. Perceba que em nenhum momento a função interior recebeu os parâmetros ou variáveis da função pai explicitamente, mas mesmo assim ela é capaz de acessá-las.

Uma closure (fechamento) trata-se de um tipo especial de objeto que combina duas coisas: a função e o ambiente onde a função foi criada. — MDN

Lembra da minha descrição de closures? Ela é uma simplificação da frase acima. O que acontece quando você cria uma closure é: a função filha guarda o contexto da função pai que está executando ela, assim ela pode acessar esse contexto quando precisar. Quando você pede uma variável que a função filho não encontra declarada dentro dela, ela vai buscar no contexto da função pai pra ver se acha. Você pode colocar quantos filhos quiser dentro da função pai ou filho, e elas sempre vão sempre percorrer todo caminho até o pai pra procurar uma variável que ela não achou.

Vamos analisar o código abaixo pra concretizar mais esse conceito:

Se você rodar isso no console do navegador ele vai mostrar “pizza” e “fusca”. Note que ambas variáveis são apenas parâmetros ou variáveis declaradas na funcão pai, mas como as funcões “filho()” e “neto()” são closures elas conseguem alcançar tudo que foi declarado em funcões que elas estão contidas independente do nível de profundidade delas.

Caso esse conceito ainda não esteja claro para você recomendo que de uma parada pra tentar entender os conceitos acima, pois agora vou mostrar como a gente utilizará esse conceito pra fazer mágica no código abaixo:

Vamos analisar por partes
— Jack o estripador

A primeira coisa que fazemos é criar uma função contador(). Essa função retorna um objeto literal que contém duas funções: “incrementar()” e “getValor()”. Ao executar a função “contador()” na linha 15 estamos retornando o objeto literal para a variável “umContador”, que agora é um objeto que tem 2 funções. Essas 2 funções acessam e modificam a variável “valor” que está na função pai. Elas conseguem fazer isso pois as duas dividem o mesmo contexto em que foram criadas, e como elas são closures elas conseguem acessar esse contexto.

E como você faria pra acessar o “valor” que está guardado la na função pai sem usar o “getValor()”? Você não consegue. Simples assim. “valor” agora é uma variável privada da instância do objeto “contador” que você criar.

É impossível acessar a variável “valor” sem ser por meio de uma closure retornada pela função pai.

Por sinal, você pode criar quantos contadores quiser:

var umContador = contador()
umContador.getValor() // => 0
umContador.incrementar()
umContador.getValor() // => 1
var outroContador = contador()
outroContador.getValor() // => 0
outroContador.incrementar()
var maisUmContador = contador()
outroContador.incrementar()
outroContador.incrementar()
outroContador.getValor() // => 3
maisUmContador.getValor() // => 0

Vamos agora pro último exemplo mostrando como misturamos tudo para criar uma função que substituiria uma classe:

agora podemos criar diferentes “tipos de personagem”:

var cavaleiro = player(5, 10, 0)
var ladrao = player(2, 7, 10)
cavaleiro.tomarDano(3) // agora o cavaleiro tem 97 de hp
ladrao.danoEmBatalha() // => 4

Perceba como rapidamente emulamos um objeto com métodos privados e públicos utilizando apenas uma característica natural das funções em JS. Existem patterns de closures mais complexas que permitem a criação de objetos de diversas maneiras, mas o objetivo desse artigo é apenas arranhar a superfície do assunto pra despertar curiosidade em quem não sabia como fazer objetos sem utilizar classes.

Para concluir, vamos listar o que sabemos sobre closures agora:

  • Elas são funções declaradas dentro de outras funções.
  • Elas podem acessar as variáveis de todas as funções de nível acima dela.
  • Elas fazem isso pois guardam os contextos das funções pai.
  • Elas dividem os mesmo contextos das outras closures.
  • E com isso podemos criar objetos com métodos privados e públicos.

Se você tiver qualquer dúvida, sugestão, ou reclamação é só comentar abaixo :)