Introdução
Closure é um comportamento padrão de toda função JavaScript e por isso é um assunto fundamental quando trabalhamos com essa linguagem. No entanto, o mais interessante sobre esse conceito, é entender quando sua aplicação pode de fato trazer um diferencial para o nosso código ou não.
Para entender Closure, vamos primeiro revisar os conceitos abaixo. Caso já tenha conhecimento sobre eles, fique a vontade para pulá-los.
- Escopo Léxico x Escopo Dinâmico
- Contexto de Execução
- Funções Aninhadas
Escopo Léxico x Escopo Dinâmico
Escopo léxico e dinâmico não são um tipo, como o de bloco ou de função, na verdade eles são duas regras diferentes que determinam como o escopo funciona em uma linguagem, ou seja, onde o JavaScript irá procurar uma variável.
Linguagens que utilizam escopo léxico sempre buscarão suas variáveis no escopo onde a função foi escrita/definida no código-fonte e não onde ela será chamada, como no exemplo abaixo:
// Exemplo em JS
const x = 10;
function external() {
const y = 20;
function internal() {
console.log(x, y);
}
internal(); // 10 20
}
Enter fullscreen mode Exit fullscreen mode
Em contrapartida, linguagens que utilizam escopo dinâmico sempre buscarão suas variáveis no escopo onde a função foi chamada/invocada, como no exemplo abaixo:
# Exemplo em Bash
x="global"
internal() {
echo "$x"
}
external() {
local x="external function"
internal
}
external # "external function"
Enter fullscreen mode Exit fullscreen mode
Importante sabermos que praticamente toda linguagem moderna, incluindo JavaScript trabalha apenas em Escopo léxico. Esse exemplo serve apenas pra aprendermos por contraste.
Contexto de Execução
É um ambiente criado pela engine JS sempre que um trecho de código é executado, guardando o que é necessário para aquele código rodar.
Existem dois tipos de Contexto de Execução:
- Contexto de Execução Global: criado uma única vez, quando o script começa.
- Contexto de Execução de Função: criado a cada chamada de função, criando um contexto novo e independente, mesmo que seja a mesma função sendo chamada várias vezes.
function createContext(value) {
const message = `Contexto ${value} criado!`;
console.log(message);
}
createContext(1); // "Contexto 1 criado!"
createContext(2); // "Contexto 2 criado!"
createContext(3); // "Contexto 3 criado!"
Enter fullscreen mode Exit fullscreen mode
Cada função chamada cria um contexto de execução novo e independente que é empilhado na Call Stack. Esse contexto possui as variáveis necessárias e uma referência ao escopo léxico externo, no entanto, é interessante saber que esse escopo externo não está contido dentro do contexto atual, existe apenas uma referência que aponta para ele, como um “endereço” para outra estrutura que continua existindo em outro lugar.
- Lexical Environment
- Environment Record (bindings: variáveis, parâmetros e funções declaradas)
- Referência ao Lexical Environment externo
Normalmente, quando uma função termina de executar, seu contexto de execução é removido da Call Stack e as variáveis locais deixam de existir, liberando espaço na memória.
Funções Aninhadas
O JavaScript permite que definamos uma função dentro de outra, criando uma função aninhada. É justamente nesse cenário que o comportamento da Closure se torna perceptível. No entanto, antes de chegar lá, vamos entender dois pontos:
- A função interna tem acesso ao escopo da função externa
- A função interna pode escapar da função externa
Acesso ao Escopo da Função Externa
O mesmo comportamento que vimos anteriormente também se aplica aqui: uma função tem acesso ao escopo onde foi definida, nesse caso, o escopo da função externa. Com isso, como a função externa tem acesso ao escopo global, formamos o que chamamos de scope chain.
Escape da Função Interna
Quando trabalhamos com funções aninhadas é interessante saber que a função interna não precisa ficar presa dentro da função externa, ela pode ser:
- Retornada
- Guardada em uma variável fora do escopo da função externa
- Passada como callback
Confira um exemplo simples do retorno da função:
function external() {
const value = 10;
return function internal() {
console.log(value);
}
}
const fn = external(); // internal escapou da external
Enter fullscreen mode Exit fullscreen mode
Agora temos todos os conceitos necessários para entender Closures. Sabemos que:
- O JS resolve variáveis pelo escopo léxico, onde foi definida e não onde foi chamado
- Cada função chamada cria um contexto de execução que normalmente é destruído assim que a função termina
- Uma função aninhada pode escapar da função que a criou, sendo retornada, armazenada e até mesmo passada como callback
A questão que falta esclarecer é: o que acontece quando essas três coisas se encontram? Quando uma função interna escapa, mas ainda depende de variáveis de um contexto que já deveria ter sido destruído? É exatamente esse comportamento que chamamos de Closure.
Closure
Imagine uma pessoa que vai passar o dia fora de casa. Antes de sair, ela arruma sua mochila colocando dentro dela tudo que sabe que vai precisar. Ela sai e vai para vários lugares diferentes de onde morava, mas continua tendo acesso a tudo aquilo que colocou na mochila, porque carrega esses itens por onde quer que vá.
Basicamente esse é o comportamento perceptível de uma closure, que ocorre sempre quando temos uma função aninhada, onde a função interna escapa do escopo onde ela foi definida. No entanto, o Environment Record daquele contexto não é removido pelo Garbage Collector, pelo fato da função interna possuir uma referência ao escopo léxico onde foi criada.
Importante saber que engines como a V8 fazem uma análise do código identificando quais variáveis são de fato referenciadas por uma função interna. Somente essas variáveis vão para uma estrutura na heap chamada
Contexte as demais são descartadas quando a função externa termina de ser executada.
Confira esse exemplo abaixo:
function createCounter(initialValue) {
let count = initialValue;
return function increment() {
count++;
console.log(count);
}
}
const counterA = createCounter(1);
counterA(); // 2
counterA(); // 3
counterA(); // 4
Enter fullscreen mode Exit fullscreen mode
Perceba que a função increment é retornada para a constante counterA e sempre que a invocamos, o valor de count continua disponível, mesmo que a chamada de createCounter() já tenha sido finalizada, provando que a variável ainda existe em memória.
// --- Contexto de Execução Independente ---
const counterA = createCounter(1);
counterA(); // 2
counterA(); // 3
counterA(); // 4
// ---
// --- Contexto de Execução Independente ---
const counterB = createCounter(100);
counterB(); // 101
counterB(); // 102
counterB(); // 103
Enter fullscreen mode Exit fullscreen mode
Note também que cada chamada da função createCounter cria um contexto de execução independente para counterA e counterB, onde cada uma mantém seu próprio estado.
Conclusão
Ao longo deste artigo, vimos que o escopo léxico determina onde o JavaScript busca uma variável, com base em onde ela foi escrita. Vimos que cada função chamada cria um contexto de execução independente, normalmente removido assim que a função termina. Também vimos que uma função aninhada pode escapar do escopo onde foi definida. Closure é o que acontece quando essas peças se encontram: uma função que carrega consigo o acesso ao escopo onde foi criada.
Entender esse mecanismo não serve apenas para entender como o código funciona, mas para saber quando vale usá-lo intencionalmente, e também reconhecer quando ele pode se tornar uma armadilha. Encapsulamento de estado privado (como o exemplo do contador), factories de funções configuráveis e o padrão de módulos são exemplos onde closure deixa de ser um comportamento e passa a ser uma ferramenta em nossas mãos. Por outro lado, é essa mesma mecânica que está por trás do bug clássico do React conhecido como stale closure, quando uma função continua “presa” a um valor antigo, porque a referência que ela guarda nunca foi atualizada. Reconhecer esse padrão é o que separa quem apenas usa closure de quem a compreende.
Referências
- Closures
- JavaScript execution model
- Functions
- Blazingly fast parsing, part 2: lazy parsing
- V8 release v7.1
Créditos de imagem
Foto de Cody Black na Unsplash
답글 남기기