Closures em JavaScript: a função que lembra

Entenda o que é uma closure, como uma função guarda acesso às variáveis de onde nasceu, onde isso aparece no dia a dia (contadores, hooks, módulos) e as armadilhas clássicas com var e loops.


Closure é um daqueles termos que aparecem em toda entrevista e que muita gente sabe usar sem saber nomear. Você já escreveu closures hoje, provavelmente, a cada callback e a cada hook. Entender o conceito explica comportamentos que de outro jeito parecem mágica ou bug.

A ideia em uma frase

Uma closure é uma função que continua tendo acesso às variáveis do lugar onde foi criada, mesmo depois que esse lugar já terminou de executar.

function criarContador() {
  let total = 0;
 
  return function () {
    total++;
    return total;
  };
}
 
const contar = criarContador();
contar(); // 1
contar(); // 2
contar(); // 3

Quando criarContador termina, o normal seria total desaparecer. Mas a função devolvida ainda usa essa variável, então o JavaScript a mantém viva. A função "lembra" do ambiente onde nasceu.

Cada closure tem a sua cópia

Cada chamada a criarContador cria um ambiente novo, com o seu próprio total:

const a = criarContador();
const b = criarContador();
 
a(); // 1
a(); // 2
b(); // 1, o contador de b é independente

Isso é o que permite criar várias instâncias com estado próprio sem usar classe.

Dados privados

Como total só é acessível por dentro, ninguém de fora consegue mexer nele diretamente. Closures são a forma clássica de ter variáveis privadas em JavaScript:

function criarConta(saldoInicial) {
  let saldo = saldoInicial;
 
  return {
    depositar(valor) {
      saldo += valor;
    },
    consultar() {
      return saldo;
    },
  };
}
 
const conta = criarConta(100);
conta.depositar(50);
conta.consultar(); // 150
conta.saldo; // undefined, não existe acesso direto

Onde você já usa closures

Callbacks e eventos. A função passada pro setTimeout ou pro addEventListener guarda as variáveis do escopo em volta:

function agendarAviso(nome) {
  setTimeout(() => {
    console.log(`Olá, ${nome}`); // "nome" vem da closure
  }, 1000);
}

Funções que geram funções. Quando você quer configurar um comportamento uma vez e reutilizar:

const multiplicarPor = (fator) => (numero) => numero * fator;
 
const dobrar = multiplicarPor(2);
dobrar(5); // 10

Hooks do React. Um componente React é uma função que roda a cada renderização, e cada função interna (um handler de clique, um useEffect) captura os valores daquela renderização específica:

function Contagem() {
  const [n, setN] = useState(0);
 
  function mostrarDepois() {
    setTimeout(() => alert(n), 3000); // mostra o valor de n de quando o clique aconteceu
  }
 
  return <button onClick={mostrarDepois}>Ver</button>;
}

Se você clicar, depois incrementar n antes dos 3 segundos, o alerta mostra o valor antigo. Não é bug, é a closure guardando o n daquela renderização. Esse mesmo mecanismo explica o famoso problema de "estado velho" dentro de useEffect com array de dependências incompleto.

A armadilha clássica: var em loop

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// imprime 3, 3, 3

Todas as funções compartilham a mesma variável i, porque var tem escopo de função, não de bloco. Quando os timeouts disparam, o loop já acabou e i vale 3. A correção é usar let, que cria um i novo a cada volta:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// imprime 0, 1, 2

O custo: memória

Como a closure mantém as variáveis vivas, elas não são coletadas pelo garbage collector enquanto a função existir. Na maioria dos casos isso é irrelevante, mas uma closure que guarda um objeto enorme e fica registrada num evento que nunca é removido causa vazamento de memória. Se você registra um listener, lembre de removê-lo quando não for mais necessário.

Fechando

Closure é a função carregando o ambiente onde foi criada. Ela explica contadores com estado privado, funções fábrica, o comportamento dos hooks e o bug do var em loops. Quando um valor aparecer "velho" ou "errado" dentro de um callback, a primeira pergunta é: de qual escopo essa função está lembrando?

Direto na sua
caixa de entrada.

Um aviso por e-mail sempre que eu publicar um post novo. Sem spam, sem newsletter chata, só isso.