Back

Como Funcionam os Closures em JavaScript

Como Funcionam os Closures em JavaScript

Você escreveu uma função dentro de outra função e, de alguma forma, a função interna ainda acessa variáveis da externa—mesmo depois que a função externa terminou de executar. Esse comportamento confunde muitos desenvolvedores, mas segue uma regra simples: closures capturam bindings, não valores.

Este artigo explica como os closures em JavaScript funcionam, o que o escopo léxico realmente significa e como evitar erros comuns que surgem da má compreensão dessas mecânicas.

Principais Conclusões

  • Um closure é uma função combinada com seu ambiente léxico—os bindings de variáveis que existiam quando a função foi criada.
  • Closures capturam bindings (referências), não valores, então mutações em variáveis capturadas permanecem visíveis.
  • Use let ou const em loops para criar bindings novos por iteração e evitar o problema clássico de loop.
  • Problemas de memória surgem de reter referências desnecessárias, não dos closures em si.

O que é um closure?

Um closure é uma função combinada com seu ambiente léxico—o conjunto de bindings de variáveis que existiam quando a função foi criada. Toda função em JavaScript forma um closure no momento da criação.

function createGreeter(greeting) {
  return function(name) {
    return `${greeting}, ${name}`
  }
}

const sayHello = createGreeter('Hello')
sayHello('Alice') // "Hello, Alice"

Quando createGreeter retorna, seu contexto de execução termina. Ainda assim, a função retornada continua acessando greeting. A função interna não copiou a string “Hello”—ela reteve uma referência ao próprio binding.

Escopo léxico em JavaScript explicado

Escopo léxico significa que o acesso a variáveis é determinado por onde as funções são definidas no código-fonte, não onde elas executam. O motor JavaScript resolve nomes de variáveis percorrendo a cadeia de escopos do escopo mais interno para fora.

const multiplier = 2

function outer() {
  const multiplier = 10
  
  function inner(value) {
    return value * multiplier
  }
  
  return inner
}

const calculate = outer()
calculate(5) // 50, não 10

A função inner usa multiplier de seu ambiente léxico—o escopo onde foi definida—independentemente de qualquer variável global com o mesmo nome.

Closures capturam bindings, não snapshots

Um equívoco comum é que closures “congelam” valores de variáveis. Eles não fazem isso. Closures mantêm referências a bindings, então mutações permanecem visíveis:

function createCounter() {
  let count = 0
  return {
    increment() { count++ },
    getValue() { return count }
  }
}

const counter = createCounter()
counter.increment()
counter.increment()
counter.getValue() // 2

Ambos os métodos compartilham o mesmo binding count. Mudanças feitas por increment são visíveis para getValue porque referenciam a mesma variável.

O problema clássico de loop: var versus let

Essa distinção importa mais em loops. Com var, todas as iterações compartilham um binding:

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

Cada callback fecha sobre o mesmo i, que é igual a 3 quando os callbacks executam.

Com let, cada iteração cria um binding novo:

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

O escopo de bloco dá a cada closure seu próprio i.

Padrões práticos: funções factory e manipuladores de eventos

Closures permitem funções factory que produzem comportamento especializado:

function createValidator(minLength) {
  return function(input) {
    return input.length >= minLength
  }
}

const validatePassword = createValidator(8)
validatePassword('secret') // false
validatePassword('longenough') // true

Manipuladores de eventos naturalmente usam closures para reter contexto:

function setupButton(buttonId, message) {
  document.getElementById(buttonId).addEventListener('click', () => {
    console.log(message)
  })
}

O callback retém acesso a message muito tempo depois que setupButton retorna.

Considerações de memória: o que closures realmente retêm

Closures não causam vazamentos de memória inerentemente. Problemas surgem quando funções retêm inadvertidamente referências a objetos grandes ou estruturas de dados de longa duração.

function processData(largeDataset) {
  const summary = computeSummary(largeDataset)
  
  return function() {
    return summary // Retém apenas summary, não largeDataset
  }
}

Se seu closure precisa apenas de um pequeno pedaço de dados, extraia-o antes de criar a função interna. O objeto grande original se torna elegível para coleta de lixo.

Motores JavaScript modernos otimizam closures agressivamente. São um recurso normal da linguagem, não uma preocupação de desempenho no uso típico. O problema não são os closures em si—é reter referências que você não precisa.

Construindo um modelo mental confiável

Pense em closures desta forma: quando uma função é criada, ela captura uma referência ao seu escopo circundante. Esse escopo contém bindings—conexões entre nomes e valores—não os valores em si. A função pode ler e modificar esses bindings durante toda sua vida útil.

Este modelo explica por que mutações são visíveis, por que let corrige problemas de loop e por que closures funcionam através de limites assíncronos. A função e seu ambiente léxico viajam juntos.

Conclusão

Closures são funções empacotadas com seu ambiente léxico. Eles capturam bindings, não valores, então mudanças em variáveis capturadas permanecem visíveis. Use let ou const em loops para criar bindings novos por iteração. Problemas de memória surgem de reter referências desnecessárias, não dos closures em si.

Entender escopo e closures em JavaScript lhe dá uma base para raciocinar sobre tempo de vida de variáveis, encapsulamento de dados e comportamento de callbacks—padrões que você encontrará diariamente no desenvolvimento frontend.

Perguntas Frequentes

Toda função em JavaScript é tecnicamente um closure porque captura seu ambiente léxico no momento da criação. O termo closure tipicamente se refere a funções que acessam variáveis de um escopo externo depois que esse escopo terminou de executar. Uma função que usa apenas suas próprias variáveis locais ou variáveis globais não demonstra comportamento de closure de forma significativa.

Closures não causam vazamentos de memória inerentemente. Problemas ocorrem quando closures retêm inadvertidamente referências a objetos grandes ou estruturas de dados que deveriam ser coletadas pelo garbage collector. Para evitar isso, extraia apenas os dados que seu closure precisa antes de criar a função interna. Motores JavaScript modernos otimizam closures agressivamente, então não são uma preocupação de desempenho no uso típico.

Isso acontece ao usar var em loops porque var tem escopo de função, não de bloco. Todas as iterações compartilham o mesmo binding, então os callbacks veem o valor final quando executam. Corrija isso usando let, que cria um binding novo para cada iteração. Cada closure então captura sua própria cópia da variável de loop.

Sim. Closures capturam bindings (referências a variáveis), não snapshots de valores. Se a variável capturada muda, o closure vê o valor atualizado. É por isso que múltiplas funções compartilhando o mesmo closure podem se comunicar através de variáveis compartilhadas, como visto no padrão de contador onde os métodos increment e getValue compartilham o mesmo binding count.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay