Closures e Escopo em JavaScript Explicados de Forma Simples
Você está escrevendo uma função JavaScript que retorna outra função. A função interna usa uma variável da função externa, mas quando você a chama depois, a função externa já terminou. No entanto, a variável ainda está lá. Como? Essa é a magia das closures e do escopo. Muitos desenvolvedores lutam com esse conceito, levando a bugs como valores inesperados de variáveis ou vazamentos de memória. Neste artigo, vamos desmistificar closures e escopo em JavaScript de forma simples com exemplos práticos.
O que é Escopo em JavaScript?
O escopo determina onde as variáveis são acessíveis no seu código. JavaScript tem três tipos principais de escopo:
- Escopo global: Variáveis declaradas fora de qualquer função ou bloco são globalmente acessíveis.
- Escopo de função: Variáveis declaradas dentro de uma função são acessíveis apenas dentro dessa função.
- Escopo de bloco: Variáveis declaradas com
leteconstdentro de um bloco{}são acessíveis apenas dentro desse bloco.
Aqui está um exemplo rápido:
let globalVar = 'I am global';
function myFunc() {
let functionVar = 'I am in function scope';
if (true) {
let blockVar = 'I am in block scope';
console.log(globalVar); // accessible
console.log(functionVar); // accessible
console.log(blockVar); // accessible
}
console.log(blockVar); // ReferenceError: blockVar is not defined
}
myFunc();
console.log(functionVar); // ReferenceError: functionVar is not defined
Entender o escopo é crucial porque evita colisões de variáveis e ajuda a gerenciar a memória.
Escopo Lexical: A Base das Closures
JavaScript usa escopo lexical, o que significa que o escopo de uma variável é determinado por sua posição no código-fonte. Funções internas têm acesso a variáveis em seu escopo externo (envolvente). Isso também é chamado de escopo estático.
Considere isto:
function outer() {
let outerVar = 'outer';
function inner() {
console.log(outerVar); // 'outer'
}
inner();
}
outer();
A função inner pode acessar outerVar porque está lexicalmente dentro de outer. Isso não é dinâmico; não depende de como a função é chamada.
O que é uma Closure?
Uma closure é uma função que lembra seu escopo lexical mesmo quando a função é executada fora desse escopo. Em outras palavras, uma closure dá acesso ao escopo de uma função externa a partir de uma função interna. Closures são criadas toda vez que uma função é criada, no momento da criação da função.
Exemplo:
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
Aqui, makeCounter retorna uma função interna que incrementa count. Mesmo depois que makeCounter terminou de executar, a função retornada ainda tem acesso a count. Isso é uma closure.
Como as Closures Funcionam nos Bastidores
Quando uma função é criada, ela armazena uma referência ao seu ambiente lexical (a cadeia de escopo). Quando a função é invocada, ela usa esse ambiente para resolver variáveis. O ambiente é mantido vivo enquanto a closure existir.
Usos Práticos de Closures
Closures não são apenas teóricas; elas são usadas em toda parte no JavaScript.
1. Privacidade de Dados (Padrão Módulo)
Antes dos módulos ES6, closures eram usadas para criar variáveis privadas:
const counterModule = (function() {
let count = 0;
return {
increment: function() { count++; },
getCount: function() { return count; }
};
})();
counterModule.increment();
console.log(counterModule.getCount()); // 1
console.log(counterModule.count); // undefined
Aqui, count é privada e acessível apenas via os métodos retornados.
2. Manipuladores de Eventos e Callbacks
Closures são comumente usadas em manipuladores de eventos para acessar variáveis do escopo externo:
function setupButton() {
let clickCount = 0;
document.getElementById('myButton').addEventListener('click', function() {
clickCount++;
console.log(`Button clicked ${clickCount} times`);
});
}
A closure do manipulador de eventos lembra clickCount.
3. Fábricas de Funções
Você pode criar funções com argumentos predefinidos:
function multiplyBy(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplyBy(2);
console.log(double(5)); // 10
Armadilhas Comuns e Como Evitá-las
Closures podem levar a comportamentos inesperados se não forem compreendidas.
Captura de Variável em Loop
Usar var em loops cria uma única ligação compartilhada:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 3, 3, 3
}, 100);
}
Todos os callbacks compartilham o mesmo i. Para corrigir, use let (escopo de bloco) ou uma IIFE:
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 0, 1, 2
}, 100);
}
Vazamentos de Memória
Closures mantêm variáveis vivas. Se uma closure estiver anexada a um objeto de longa duração (como um elemento DOM), ela pode impedir a coleta de lixo. Sempre remova os ouvintes de eventos quando não forem mais necessários.
Cadeia de Escopo e Resolução de Variáveis
Quando você acessa uma variável, o JavaScript procura na cadeia de escopo: primeiro o escopo atual, depois os escopos externos, até o escopo global. Se não for encontrada, um ReferenceError é lançado.
Exemplo:
let a = 'global';
function outer() {
let b = 'outer';
function inner() {
let c = 'inner';
console.log(a, b, c); // all accessible
}
inner();
}
outer();
A cadeia de escopo para inner é: inner -> outer -> global.
Comparação: var, let e const
| Palavra-chave | Escopo | Hoisting | Reatribuível |
|---|---|---|---|
| var | Função | Sim (inicializado como undefined) | Sim |
| let | Bloco | Sim (mas não inicializado, TDZ) | Sim |
| const | Bloco | Sim (mas não inicializado, TDZ) | Não |
Entender essas diferenças é fundamental para evitar bugs relacionados a escopo.
Perguntas Frequentes
O que é uma closure em JavaScript?
Uma closure é uma função que mantém acesso ao seu escopo lexical mesmo quando executada fora desse escopo. Ela permite que uma função "lembre" variáveis de onde foi definida.
Por que as closures são importantes?
Closures possibilitam privacidade de dados, fábricas de funções e manipuladores de eventos que mantêm estado. Elas são fundamentais para muitos padrões JavaScript.
Como posso evitar bugs relacionados a closures?
Use let e const para escopo de bloco, esteja atento a loops e limpe os ouvintes de eventos para evitar vazamentos de memória.
Pronto para testar seu entendimento? Experimente nosso Formatador JSON para experimentar trechos de código e ver como as closures funcionam em tempo real.