O Que É uma Closure em JavaScript?
| |
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:
| |
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:
| |
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:
| |
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:
| |
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:
| |
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:
| |
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.