O Que É uma Closure em JavaScript?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function makeCounter() {
  let count = 0;
  return function () {
    count += 1;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

makeCounter executa uma vez, retorna uma função, e aí termina: seu stack frame não existe mais. Ainda assim, cada chamada a counter() continua lendo e atualizando count, uma variável declarada dentro de uma função que já terminou. count não volta para 0, e não é copiada. É a mesma variável, ainda viva, três chamadas depois.

Isso é uma closure: uma função emparelhada com uma referência às variáveis que estavam no seu escopo no momento em que foi definida. Não é uma foto dos valores: é uma referência viva. Se outra coisa mudar count, o contador vê o novo valor.

Por Que a Variável Sobrevive Depois que a Função Termina?

O JavaScript resolve nomes de variáveis lexicamente, pelo lugar onde estão escritas no código, não por quem chama a função. Para count += 1 dentro da função interna, o motor procura nos escopos externos até achar uma declaração, e encontra count em makeCounter. Esse vínculo é fixado onde a função interna é definida, não onde ela é chamada.

Normalmente, quando uma função termina, nada mais alcança suas variáveis locais e o garbage collector as recupera. Uma closure é a exceção. A função retornada mantém uma referência ao escopo em que foi criada, então esse escopo permanece vivo enquanto a função existir. Chame makeCounter() de novo e você obtém um segundo contador com seu próprio count em seu próprio escopo:

1
2
3
4
5
const a = makeCounter();
const b = makeCounter();
console.log(a()); // 1
console.log(a()); // 2
console.log(b()); // 1 — escopo separado, count separado

Toda função que você cria em JavaScript fecha sobre o escopo que a envolve, independentemente de você usar isso ou não. Normalmente nada sobrevive à chamada que a criou, então você nunca percebe. Isso importa quando você retorna a função interna, passa ela para outro lugar, ou a armazena: ela continua funcionando mesmo depois que o escopo do pai já teria desaparecido.

Como Criar Variáveis Privadas com uma Closure?

O contador já mostra o uso principal: dados que só a função retornada pode acessar. Nada fora de makeCounter pode ler ou definir count. Não existe propriedade para isso, nem nada para inspecionar no console. Você obtém o mesmo encapsulamento de um campo privado de classe, construído a partir de uma única chamada de função:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function createAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit(amount) {
      balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error("insufficient funds");
      balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    },
  };
}

const account = createAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
console.log(account.balance); // undefined — inacessível

Os três métodos fecham sobre o mesmo balance, então o compartilham. Código externo só consegue alcançá-lo pelos métodos que você escolheu expor. Esse é o module pattern: envolver estado privado e uma interface pública em uma única chamada de função, retornando apenas a interface.

Como Memoizar uma Função com uma Closure?

Feche sobre um Map e você tem um wrapper de memoização, com um cache compartilhado entre as chamadas à função retornada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function memoize(fn) {
  const cache = new Map();
  return function (arg) {
    if (cache.has(arg)) return cache.get(arg);
    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

function slowSquare(n) {
  for (let i = 0; i < 1e8; i++) {} // finja que isso é custoso
  return n * n;
}

const fastSquare = memoize(slowSquare);
fastSquare(5); // executa o loop, guarda 25 no cache
fastSquare(5); // retorna 25 do cache, sem loop

Os dois exemplos seguem a mesma forma: você chama uma função que prepara algum estado, e recebe de volta uma ou mais funções que continuam usando esse estado. O cache aqui nunca descarta nada, então não envolva assim uma função que recebe entradas distintas sem limite. Os decoradores Python seguem o mesmo mecanismo: um decorador fecha sobre a função que ele envolve e retorna um substituto que ainda pode chamá-la.

Por Que var em um Loop Quebra Closures?

Esse é o erro de closure que quase todo mundo comete uma vez:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const callbacks = [];
for (var i = 0; i < 3; i++) {
  callbacks.push(function () {
    console.log(i);
  });
}
callbacks.forEach((cb) => cb());
// 3
// 3
// 3

Você queria 0 1 2. Obteve 3 3 3. var tem escopo de função, não de bloco, então existe exatamente um i para todo o loop, e as três callbacks fecham sobre ele. Quando qualquer uma delas executa, o loop já terminou e i é 3.

Declare com let e o bug desaparece:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const callbacks = [];
for (let i = 0; i < 3; i++) {
  callbacks.push(function () {
    console.log(i);
  });
}
callbacks.forEach((cb) => cb());
// 0
// 1
// 2

let tem escopo de bloco, e um loop for com let no cabeçalho cria um binding separado para cada iteração. Três i distintos. Cada callback fecha sobre o da sua própria iteração.

Antes do let, a correção era uma IIFE que forçava um novo escopo por iteração passando i como parâmetro:

1
2
3
4
5
6
7
for (var i = 0; i < 3; i++) {
  (function (j) {
    callbacks.push(function () {
      console.log(j);
    });
  })(i);
}

Você ainda encontra isso em código antigo. Não escreva assim hoje: coloque let no cabeçalho do loop.

Closures Podem Causar Vazamentos de Memória?

Sim. Uma closure mantém o ambiente lexical real onde suas variáveis vivem, por todo o tempo em que a própria closure for alcançável. Associe uma a algo de vida longa, como um event listener do DOM, um array no nível do módulo, ou um cache sem eviction, e tudo que esse ambiente carrega também fica na memória.

O caso comum: um listener em um nó do DOM fecha sobre um objeto grande que o handler quase não usa. O nó é removido da página, o listener nunca é desanexado, e tanto o nó quanto o objeto continuam vivos porque a closure ainda os referencia. Chame removeEventListener quando o elemento desaparecer. E mantenha closures bem restritas: se um callback precisa de apenas um campo, feche sobre esse campo, não sobre o objeto inteiro.

O segundo custo é mais silencioso. Um método definido dentro de um construtor, em vez de no prototype, é recriado com sua própria closure a cada instância, em vez de ser compartilhado entre todas. Se a função interna não captura nada da externa, não a aninhe.

Quando Usar uma Closure?

Você provavelmente já usou alguma. Um debounce ou throttle em JavaScript retorna um wrapper que fecha sobre um timeoutId que persiste entre chamadas, e é assim que ele cancela a chamada anterior ainda pendente. Qualquer callback que você passe para setTimeout ou um handler de promise que leia uma variável da função que o agendou funciona do mesmo jeito: o event loop o executa bem depois que essa função já terminou.

Use uma closure de propósito quando precisar de estado que persista entre chamadas mas não deva ser exposto como propriedade: um contador, um cache, um timer de debounce, uma função configurada uma vez e chamada depois. Para um utilitário sem estado que só transforma seus argumentos, pule o wrapper. Não há nada para fechar sobre, e o aninhamento adiciona um escopo com o qual você precisa lidar sem ganho nenhum.

Artigos relacionados