Escopo Léxico: O Alicerce das Closures
O escopo léxico é a regra fundamental que governa como as variáveis são resolvidas em JavaScript. Ele significa que o escopo é determinado pela posição do código no arquivo, não por onde a função é chamada. Quando você declara uma função, ela "lembra" do ambiente em que foi definida, criando uma cadeia de escopos que pode ser consultada em tempo de execução.
const global = "Sou global";
function externa() {
const doEscopo = "Sou do escopo da função externa";
function interna() {
const local = "Sou local";
console.log(local); // "Sou local"
console.log(doEscopo); // "Sou do escopo da função externa"
console.log(global); // "Sou global"
}
interna();
}
externa();
Neste exemplo, interna() acessa variáveis de três níveis diferentes: seu próprio escopo, o escopo de externa() e o escopo global. O JavaScript busca essas variáveis seguindo a cadeia de escopos, sempre começando pelo mais próximo. Essa característica é chamada de shadowing quando uma variável interna tem o mesmo nome de uma externa — a interna prevalece.
Closures: Capturando o Escopo
Uma closure é uma função que "captura" variáveis do seu escopo envolvente e as mantém vivas, mesmo após a função externa ter terminado sua execução. Contrário ao que muitos pensam, closures não são um recurso especial — elas são o comportamento padrão de toda função em JavaScript.
function contador() {
let count = 0;
return function() {
count++;
return count;
};
}
const incrementar = contador();
console.log(incrementar()); // 1
console.log(incrementar()); // 2
console.log(incrementar()); // 3
Aqui, a função retornada é uma closure que captura a variável count. Mesmo após contador() ter terminado, a variável count não é descartada pelo garbage collector — ela continua viva enquanto a função retornada existir. Cada chamada a incrementar() modifica e retorna o novo valor de count. Se você criar outra variável com contador(), ela terá sua própria instância isolada de count.
const contador1 = contador();
const contador2 = contador();
console.log(contador1()); // 1
console.log(contador1()); // 2
console.log(contador2()); // 1 — nova instância!
Funções de Primeira Classe e Padrões Práticos
Em JavaScript, funções são cidadãs de primeira classe: podem ser atribuídas a variáveis, passadas como argumentos e retornadas como valores. Essa característica, combinada com closures, permite padrões poderosos como factory functions, decorators e módulos.
Factory Functions
Factory functions retornam novos objetos, frequentemente usando closures para encapsular dados privados:
function criarPessoa(nome, idade) {
// Dados privados
let _idade = idade;
return {
getNome() { return nome; },
getIdade() { return _idade; },
fazerAniversario() { _idade++; }
};
}
const joao = criarPessoa("João", 30);
console.log(joao.getNome()); // "João"
console.log(joao.getIdade()); // 30
joao.fazerAniversario();
console.log(joao.getIdade()); // 31
Neste padrão, _idade é privada — só pode ser acessada através dos métodos retornados. Isso é encapsulamento real sem necessidade de classes.
Decorators
Decorators usam closures para envolver funções e modificar seu comportamento:
function logger(funcao) {
return function(...args) {
console.log(`Chamando ${funcao.name} com:`, args);
const resultado = funcao.apply(this, args);
console.log(`Resultado:`, resultado);
return resultado;
};
}
function somar(a, b) {
return a + b;
}
const somarComLog = logger(somar);
somarComLog(5, 3);
// Chamando somar com: [5, 3]
// Resultado: 8
O decorator logger retorna uma closure que captura a função original, permitindo executar código antes e depois dela. Esse padrão é usado extensivamente em frameworks modernos.
Padrão de Módulo
Closures são a base do padrão de módulo, que cria espaços privados sem poluir o escopo global:
const calculadora = (function() {
// Dados e funções privadas
const historico = [];
function registrar(operacao) {
historico.push(operacao);
}
// API pública
return {
somar(a, b) {
const resultado = a + b;
registrar(`${a} + ${b} = ${resultado}`);
return resultado;
},
getHistorico() {
return [...historico];
}
};
})();
console.log(calculadora.somar(2, 3)); // 5
console.log(calculadora.getHistorico()); // ["2 + 3 = 5"]
// console.log(calculadora.historico); // undefined — privado!
A função anônima auto-executada cria um escopo privado. Apenas os métodos retornados têm acesso a historico e registrar, implementando verdadeiro encapsulamento.
Armadilhas Comuns e Boas Práticas
Um erro frequente ocorre em loops quando você tenta capturar valores mutáveis. No exemplo abaixo, todas as closures compartilham a mesma variável i:
// ❌ Errado
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(() => console.log(i));
}
funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3
Ao invocar as funções, i já vale 3. A solução é usar let ou criar uma closure adicional:
// ✅ Correto com let
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(() => console.log(i));
}
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2
// ✅ Ou com IIFE (função auto-executada)
const funcs2 = [];
for (var i = 0; i < 3; i++) {
funcs2.push((function(j) {
return () => console.log(j);
})(i));
}
funcs2[0](); // 0
Com let, cada iteração cria um novo escopo, capturando uma nova instância de i. Com IIFE, passamos i como argumento, criando uma closure sobre um parâmetro que não muda.
Conclusão
Closures são o mecanismo fundamental que permite encapsulamento, abstrações seguras e padrões de design poderosos em JavaScript. O escopo léxico garante que funções sempre acessem o ambiente correto, independente de onde são executadas. Quando você domina closures e funções de primeira classe, pode criar código mais modular, testável e mantível — desde factory functions até decorators e módulos auto-contidos. A chave é entender que toda função em JavaScript é uma closure, e aprender a usá-las intencionalmente para resolver problemas de forma elegante.