O que são Closures?

Entenda o que são closures, o que é o escopo léxico e como tirar proveito disso.

Introdução

É comum utilizarmos frameworks ou mesmo o Vanilla JavaScript sem entendermos alguns conceitos que podem nos ajudar bastante durante o nosso dia-a-dia como desenvolvedor. Hoje vamos ver o conceito de closures, como podem compartilhar o mesmo escopo léxico e o que isso pode nos trazer de benefício durante o desenvolvimento. Para isso, utilizarei, como referência principal, o artigo da MDN sobre closures. Sem mais delongas, vamos lá!

Em resumo, o que é uma Closure?

Uma closure é a combinação de uma função aninhada com referência ao estado que a circunda (o escopo léxico). Dito de outro modo: uma closure te dá acesso ao escopo de uma função externa a partir de uma função interna. Em Javascript, closures são criadas toda vez que uma função é criada.

Escopo Léxico

A melhor forma de explicar o que é um escopo léxico é através de um exemplo:

function showClosureBehaviour() {
  let localValue = 'Some test value'; //variável local criada pela função

  function logValue() {
    //função interna => uma closure
    console.log(localValue); //utiliza variável declarada na função pai
  }

  logValue();
}

showClosureBehaviour(); // Imprime "Some test value"

Perceba que a função logValue não possui nenhuma variável própria. Porém, como funções internas (ou "filhas") têm acesso às variáveis de funções externas (funções "pai"), logValue consegue acessar a variável localValue que foi declarada na função pai. Esse é justamente um exemplo de escopo léxico. A palavra léxico se refere justamente ao fato de que o escopo usa a localização onde a variável é declarada dentro do código fonte para determinar onde aquela variável está disponível. Então, guarde isso: funções aninhadas têm acesso a variáveis declaradas no escopo mais externo.

Entendendo melhor o que é uma Closure

Para entender melhor o que é uma closure, vejamos um exemplo muito parecido com o anterior, mas com uma pequena diferença.

function showClosureBehaviour() {
  let localValue = 'Some test value'; //variável local criada pela função

  function logValue() {
    //função interna => uma closure
    console.log(localValue); //utiliza variável declarada na função pai
  }

  return logValue; //retorna a closure criada
}
let newFunc = showClosureBehaviour();
newFunc(); // Imprime "Some test value"

É possível perceber que a única diferença desse trecho de código para o anterior é que a função filha (logValue) é retornada a partir da função pai. Em algumas linguagens de programação não seria possível executar a função newFunc, visto que as variáveis locais dentro da função só existiram durante a execução da função showClosureBehaviour. Entretanto, isso não acontece em Javascript, visto que funções formam closures.

Uma closure é a combinação de uma função e o escopo léxico dentro do qual a referida função foi declarada. Esse escopo inclui quaisquer variáveis que estavam no escopo no momento em que a closure foi criada. Ou seja, newFunc é uma referência para a instância da função logValue que foi criada quando showClosureBehaviour foi executada. Nesse caso, a instância de logValue mantém a referência para o seu escopo léxico, dentro do qual a variável localValue existe. Logo, quando newFunc é invocada, a variável localValue permanece disponível para uso e "Some test value" é impresso em tela.

Blz, mas...para que preciso saber disso? Qual a importância?

Uma vez sabendo o que é uma closure e o que é o escopo léxico, é possível, dentre outras coisas, emular métodos privados. Diferentemente de uma linguagem como Java (orientada a objetos), JavaScript não provê uma forma nativa de declarar métodos privados. Tais métodos são úteis porque restringem o acesso ao seu código, evitando assim que métodos auxiliares venham a bagunçar a interface pública do seu código.

Vejamos uma forma de usar closures para definir funções públicas que podem acessar funções e variáveis privadas (conhecido como Module Pattern):

const makeCounter = function() {
  //Semelhante à criação de uma classe em POO
  var privateCounter = 0; //Faz parte do escopo léxico compartilhado
  function changeBy(val) {
    //Faz parte do escopo léxico compartilhado
    privateCounter += val;
  }

  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    },
  };
};

let counter1 = makeCounter(); //Semelhante à criação de uma instância de um objeto em POO
let counter2 = makeCounter(); //Semelhante à criação de uma instância de um objeto em POO

console.log(counter1.privateCounter); //undefined
console.log(counter1.changeBy); //undefined

console.log(counter1.value()); /* Imprime 0 */
counter1.increment();
counter1.increment();
console.log(counter1.value()); /* Imprime 2 */

console.log(counter2.value()); /* Imprime 0 */

Nesse exemplo, um único escopo léxico é criado e compartilhado por 3 funções: increment, decrement e value. Esse escopo, por sua vez, contém 2 itens privados: uma variável chamada privateCounter e uma função chamada changeBy. Nesse caso, nenhum dos 2 itens podem ser acessados diretamente, mas somente através das 3 funções públicas que são retornadas. Ou seja, essas 3 funções são closures que compartilham o mesmo escopo, tendo acesso à variável privateCounter e à função changeBy.

Outra coisa interessante a se perceber é que counter1 e counter2 são independentes entre si e cada uma mantém uma versão diferente da variável privateCounter através da sua própria closure. Cada vez que um desses contadores é chamado, seu escopo léxico muda, mudando assim o valor da variável privateCounter. No entanto, mudanças do valor da variável em uma closure não afetam o valor em outra closure. Ou seja, esse comportamento é exatamente o que temos quando criamos instâncias de classes em POO (Programação Orientada a Objetos).

Conclusão

Através do conhecimento do que é uma closure e do que é o escopo léxico, podemos ter acesso a uma série de benefícios que, normalmente, são associados com a POO (principalmente o encapsulamento). Existem, inclusive, vários outros casos de uso práticos obtidos através do uso de closures. Se quiser saber mais a respeito, você pode consultar o artigo da MDN que trata de closures que utilizei como base para criação do presente artigo.

Espero que o conhecimento adquirido durante esse artigo possa ser útil no seu dia-a-dia de programador Javascript xD. Se ficar com alguma dúvida, quiser discutir algo ou mesmo corrigir algum detalhe, fique à vontade para deixar seu comentário logo abaixo. Terei o maior prazer em respondê-lo!

Nos vemos no próximo artigo! Até lá xD.

Comments