Entendendo closures no Javascript
Definição rápida para quem veio do google em busca de “O que são closures?”
Um closure (fechamento) é uma função que se “lembra” do ambiente — ou escopo léxico — em que ela foi criada.
Ou seja, é uma função que é capaz de se lembrar do ambiente variável em que foi definida.
Também conhecidas como “Closed over Variable Ambiente” (C.O.V.E) e “Persistent Lexical Scope Referenced Data” (P.L.S.R.D).
Tá, mas o que isso significa em termos de programação?
Quando executamos uma função no Javascript, essa função cria um contexto de execução para ela, com uma memória local, ambiente variável e um estado.
O que acontece é que quando a execução dessa função é concluída, todo esse contexto é excluído, inclusive a sua memória local (que possue todos os argumentos que passamos para essa função).
Exceto o valor que retornamos nela.
Mas e se fosse possível criarmos “funções com memórias”?
Funções que conseguem persistir dados, como se pudéssemos armazenar um estado para ela?
É ai que entra um dos conceitos mais poderosos do Javascript, as closures.
Funções com memória 🧠
Isso pode parecer um pouco abstrato ainda (em como as closures são poderosas), mas tenha em mente que diversos conceitos utilizam closures por trás, como funções de memoization, module pattern, iterators, currying e muito mais.
Aplicando o conceito de closure 🔥
Preste atenção nos trechos de código abaixo, o que iremos fazer se chama function decorator, que nos torna capaz de “editarmos” uma função (não é o tópico no momento, logo farei uma série de artigos sobre programação funcional).
const multiplyBy2 = (number) => number * 2;
Ok, nada de novo até agora. Criamos uma função que recebe um parâmetro e multiplica ele por dois.
Suponhamos agora que nós quiséssemos fazer com que essa nossa função de multiplicação, em um dado contexto, possa ser utilizada apenas uma vez.
Conseguimos criar um counter dentro dela, para que caso seja > 0 não execute? Já vimos que isso não é possível, pois toda vez que ela terminar de executar a sua memória local será descartada, fazendo com que o counter volte a ser 0 em toda execução.
Vamos então criar uma nova função.
const oncefy = (fn) => {
const counter = 0;
const myFunction = () => {
if (counter === 0) {
fn();
return;
}
console.log("Only one time");
}
}
Nossa função oncefy recebe uma função como parâmetro, define um counter e verifica se é igual a 0, caso seja ele executa o nosso argumento, senão ele printa no console.
Vamos aplicar o conceito de closure em nossa função multiplyBy2, passando ela como argumento para a nossa função oncefy, que será responsável de memorizar o nosso counter.
const multiplyBy2 = (number) => {
console.log(number * 2);
}
const oncefy = (fn) => {
let counter = 0;
const myFunction = (number) => {
if (counter === 0) {
fn(number);
counter++;
return;
}
console.log("🍃");
}
return myFunction;
}
const multiplyBy2Once = oncefy(multiplyBy2);
multiplyBy2Once(3);
multiplyBy2Once(3);
Agora nossa função multiplyBy2Once será executada apenas uma vez, e nunca mais.
Nós “editamos” a nossa função multiplyBy2, dando um novo comportamento a ela, sem que precisássemos alterar o código de nossa função original, fazendo com que ela se mantenha reutilizável em outros lugares do nosso código.
Como funciona ⚙️
Vamos passo a passo entender como o nosso código está funcionando para entendermos de fato uma closure.
-
Armazenamos uma const chamada multiplyBy2 na memória global, e seu valor é uma função que recebe um parâmetro chamado number e retorna number * 2.
-
Armazenamos uma const chamada oncefy na memória na memória global, e seu valor é uma função que recebe um parâmetro chamado fn e retorna uma const chamada de myFunction.
-
Declaramos uma const com nome multiplyBy2Once, e seu valor não sabemos ainda, pois agora precisamos executar a função oncefy para atribuir o seu retorno como valor.
-
No momento a função oncefy é executada, o interpretador cria um novo contexto de execução para essa função. A primeira coisa que ele faz é pegar todos os parâmetros da função (nesse caso, o multiplyBy2) e armazenar na memória local desse contexto. Agora a memória local tem uma const chamada de fn que possui como valor a função multiplyBy2. O próximo passo será pegar todas as declarações dentro da função e armazenar na i. Então ele irá armazenar uma let com nome de counter e atribuir a ela o valor 0. Após declarar counter, nosso interpretador irá declarar uma const chamada myFunction, e seu valor é uma função que recebe um parâmetro chamado number. Após todas as declarações, ela finalmente retorna a const myFunction.
-
Ao terminar todo esse processo e retornar nosso valor (a função myFunction), todo o contexto de execução (incluindo a memória) é excluído, exceto o valor retornado, que agora será o valor da constante multiplyBy2Once.
-
Passamos para a próxima linha do nosso código, onde executamos a função multiplyBy2Once, que na realidade é a execução da função myFunction retornada.
-
Um novo contexto de execução é criado. Novamente a primeira coisa que o interpretador fará é pegar os parâmetros da função e armazenar na memória local do contexto de execução.
-
Agora a nossa memória local tem um parâmetro chamado de number, que possui como valor o nosso argumento passado (6).
É nesse momento que as coisas ficam interessante.
Na próxima linha de execução temos a condicional if (counter === 0).
Então o interpretador vai até a memória local procurar pela variável counter. Mas essa variável não existe no nosso contexto de execução atual.
Sabemos então que o interpretador irá procurar por essa variável em outro escopo, mas onde?
Se ele for para a nossa memória global, também não irá encontrar.
Porém não existe mais a memória local da nossa função oncefy, onde está declarada a nossa constante counter, pois a memória foi excluída assim que a função terminou de ser executada.
É ai então que entramos no vínculo de closure.
Quando uma função é definida (myFunction) ela se liga a memória local circundante (ambiente variável) do local em que ela foi definida (oncefy).
Ou seja, por ter sido definida dentro de oncefy, a função myFunction, “guarda” toda a memória local de onde ela está, inclusive da const counter.
Assim, quando o interpretador não encontra counter no contexto de execução atual, ele sobre imediatamente para a “mochila” que myFunction carrega consigo do contexto onde foi definida (oncefy).
Por manter essa memória, esse contexto não será apagado, lembrando sempre dos valores de sua última execução.
Conclusão 💡
Closure é um conceito muito poderoso na programação e pode ser utilizado para muitas coisas.
Ter o seu entendimento por completo pode não ser uma tarefa fácil (e difícil de se explicar também).
Porém, é importante para que se possa entender conceitos mais complexos e desenvolver soluções poderosas.
Veja também:
Entendendo Higher Order Functions no Javascript
Entendendo Classes e Prototype no Javascript
Meu website pessoal