Escopo e closures
Nesta aula, exploramos escopo léxico e closures em JavaScript, desde conceitos fundamentais até casos de uso reais e pegadinhas comuns. Você entenderá como o escopo é determinado pela estrutura do código e como closures permitem que funções 'lembrem' do ambiente onde foram criadas, com exemplos práticos e exercícios.
Escopo e closures são conceitos fundamentais em JavaScript que definem como variáveis são acessadas e como funções capturam seu contexto de criação. Compreender esses tópicos é essencial para escrever código previsível, evitar bugs sutis e dominar padrões como módulos e callbacks. Nesta aula, abordaremos desde a teoria do escopo léxico até as pegadinhas mais comuns com closures.
O escopo léxico (ou estático) determina a visibilidade de variáveis com base na posição das funções no código-fonte. Já closures são funções que 'lembram' do escopo onde foram criadas, mesmo após esse escopo ter sido executado. Vamos mergulhar em cada um desses tópicos com exemplos práticos.
Escopo léxico
Escopo léxico significa que o escopo de uma variável é definido pela sua localização no código-fonte durante a fase de compilação (antes da execução). Em JavaScript, funções criam um novo escopo, e variáveis declaradas com let e const têm escopo de bloco, enquanto var tem escopo de função.
Quando uma função é definida dentro de outra, a função interna pode acessar variáveis da função externa, mas o contrário não é verdadeiro. Isso cria uma cadeia de escopos (scope chain).
let x = 1; // escopo global
function outer() {
let y = 2; // escopo de outer
function inner() {
let z = 3; // escopo de inner
console.log(x + y + z); // acessa x (global), y (outer), z (inner)
}
inner();
}
outer(); // 6
// console.log(y); // ReferenceError: y is not definedNo exemplo acima, inner tem acesso a y de outer e a x global. Isso é possível porque o escopo é determinado pela estrutura do código, não pela execução.
O que é um closure
Um closure é uma função que mantém uma referência ao seu escopo léxico, mesmo quando a função é executada fora desse escopo. Em outras palavras, é uma função que 'lembra' do ambiente onde foi criada, incluindo variáveis locais que estavam no escopo naquele momento.
Em JavaScript, closures são criados toda vez que uma função é definida dentro de outra função e a função interna é retornada ou passada como argumento. A função interna 'fecha' o escopo externo, preservando as variáveis.
function criarContador() {
let contador = 0;
return function() {
contador++;
return contador;
};
}
const meuContador = criarContador();
console.log(meuContador()); // 1
console.log(meuContador()); // 2
console.log(meuContador()); // 3
// A variável contador não é acessível diretamente
// console.log(contador); // ReferenceErrorA função retornada por criarContador é um closure. Ela mantém acesso à variável contador mesmo após criarContador ter terminado. Cada chamada a meuContador incrementa o mesmo contador.
Casos de uso reais
Closures são amplamente utilizados em JavaScript para encapsulamento, funções de fábrica, callbacks e módulos. Um exemplo comum é o padrão de módulo, onde variáveis privadas são expostas através de métodos públicos.
function criarCarteira() {
let saldo = 0;
return {
depositar: function(valor) {
saldo += valor;
},
sacar: function(valor) {
if (valor <= saldo) {
saldo -= valor;
} else {
console.log('Saldo insuficiente');
}
},
obterSaldo: function() {
return saldo;
}
};
}
const minhaCarteira = criarCarteira();
minhaCarteira.depositar(100);
minhaCarteira.sacar(30);
console.log(minhaCarteira.obterSaldo()); // 70
// console.log(minhaCarteira.saldo); // undefinedOutro caso é em funções de alta ordem, como setTimeout ou event listeners, onde closures preservam o estado no momento da criação.
function criarSaudacao(nome) {
return function() {
console.log('Olá, ' + nome);
};
}
const saudacaoJoao = criarSaudacao('João');
setTimeout(saudacaoJoao, 1000); // após 1s: 'Olá, João'Também são essenciais em loops com var (antes do let), para capturar o valor correto da iteração.
Pegadinhas
Uma pegadinha clássica com closures ocorre quando criamos funções dentro de um loop usando var. Como var tem escopo de função, todas as funções compartilham a mesma variável, resultando em valores inesperados.
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // imprime 3, 3, 3
}, 100);
}Isso acontece porque o closure captura a variável i (que é a mesma para todas as iterações) e, quando o timeout executa, o loop já terminou e i vale 3. A solução é usar let (que tem escopo de bloco) ou criar um novo escopo com uma IIFE.
// Solução com let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 100);
}
// Solução com IIFE
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0, 1, 2
}, 100);
})(i);
}Outra pegadinha é o vazamento de memória: closures que mantêm referências a objetos grandes podem impedir a coleta de lixo, causando consumo excessivo de memória. Sempre que possível, evite armazenar referências desnecessárias em closures.
Boas práticas
Prefira let e const a var para evitar problemas de escopo. Use closures para encapsulamento, mas tome cuidado com o consumo de memória em closures de longa duração. Em loops, capture o valor com let ou IIFE para evitar o problema clássico.
Referências
- MDN: Closures
- MDN Guia: Closures
- MDN: let
- MDN: const
- MDN: Arrow functions (escopo léxico)
- JavaScript.info: Closure
- FreeCodeCamp: Entendendo o escopo em JavaScript
Exercícios
Crie uma função
criarMultiplicadorque recebe um númerone retorna uma função que multiplica seu argumento porn. Teste comconst dobrar = criarMultiplicador(2); console.log(dobrar(5));.✓ Resposta:function criarMultiplicador(n) { return function(x) { return x * n; }; } const dobrar = criarMultiplicador(2); console.log(dobrar(5)); // 10Explique por que o código abaixo imprime 3, 3, 3 e corrija-o usando
let.for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 100); }✓ Resposta:O código imprime 3, 3, 3 porque
var item escopo de função, não de bloco. O closure captura a mesma variáveli, que após o loop vale 3. Corrigindo comlet:for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 100); }Implemente uma função
criarSaudacaoque recebe um nome e retorna uma função que, ao ser chamada, exibe 'Olá, [nome]'. Use closures.✓ Resposta:function criarSaudacao(nome) { return function() { console.log('Olá, ' + nome); }; } const saudacaoMaria = criarSaudacao('Maria'); saudacaoMaria(); // 'Olá, Maria'Qual é a saída do código abaixo? Explique.
function outer() { let x = 10; function inner() { return x; } x = 20; return inner; } const fn = outer(); console.log(fn());✓ Resposta:A saída é
20. O closureinnercaptura a variávelxpor referência, não por valor. Quandoouteré executada,xé alterada para 20 antes de retornarinner. Portanto,fn()retorna o valor atual dex, que é 20.Crie uma função
criarContadorque aceita um valor inicial e retorna um objeto com métodosincrementar,decrementareobterValor. Use closures para manter o valor privado.✓ Resposta:function criarContador(inicial = 0) { let valor = inicial; return { incrementar: function() { valor++; }, decrementar: function() { valor--; }, obterValor: function() { return valor; } }; } const contador = criarContador(10); contador.incrementar(); contador.incrementar(); contador.decrementar(); console.log(contador.obterValor()); // 11