Escopo e closures
Nesta aula, exploramos o conceito de escopo léxico em JavaScript, entendendo como variáveis são acessadas com base na estrutura do código. Aprofundamos o conceito de closures, mostrando como funções retêm acesso ao escopo onde foram criadas, e apresentamos casos de uso reais como factory functions e módulos. Também discutimos pegadinhas comuns, como closures em loops, e como evitá-las.
Escopo e closures são conceitos fundamentais em JavaScript que determinam como e onde variáveis podem ser acessadas. O escopo define a visibilidade das variáveis, enquanto closures permitem que funções "lembrem" do ambiente onde foram criadas, mesmo após esse ambiente ter sido destruído. Nesta aula, vamos explorar esses tópicos em profundidade, com exemplos práticos e situações reais.
Entender escopo e closures é essencial para escrever código JavaScript robusto e evitar bugs sutis. Esses conceitos estão na base de padrões como módulos, factory functions e callbacks. Vamos começar pelo escopo léxico.
Escopo léxico
Escopo léxico (ou estático) significa que o escopo de uma variável é determinado pela sua posição no código-fonte, ou seja, onde ela é declarada. Em JavaScript, cada função cria seu próprio escopo, e funções aninhadas podem acessar variáveis dos escopos externos. Isso é conhecido como cadeia de escopos (scope chain).
Por exemplo, considere o código abaixo:
let global = "global";
function externa() {
let externaVar = "externa";
function interna() {
let internaVar = "interna";
console.log(global); // "global"
console.log(externaVar); // "externa"
console.log(internaVar); // "interna"
}
interna();
}
externa();Aqui, a função interna tem acesso às variáveis do seu próprio escopo, do escopo de externa e do escopo global. A cadeia de escopos é resolvida no momento da definição da função, não da execução. Isso é o escopo léxico.
É importante notar que variáveis declaradas com let e const têm escopo de bloco, enquanto var tem escopo de função. O escopo léxico se aplica a todas elas.
O que é um closure
Um closure é uma função que "lembra" do seu escopo léxico, mesmo quando a função é executada fora desse escopo. Em outras palavras, um closure é uma função que mantém referências a variáveis do escopo onde foi criada, mesmo após a função externa ter terminado.
Toda função em JavaScript é um closure, mas o termo é geralmente usado para funções que acessam variáveis de escopos externos após o retorno da função externa. Exemplo:
function criarContador() {
let contador = 0;
return function() {
contador++;
return contador;
};
}
const meuContador = criarContador();
console.log(meuContador()); // 1
console.log(meuContador()); // 2A função anônima retornada por criarContador é um closure. Ela mantém uma referência à variável contador do escopo de criarContador. Mesmo após criarContador ter terminado, contador ainda existe e pode ser incrementado.
Casos de uso reais
Closures são amplamente utilizados em JavaScript. Alguns casos comuns:
- Factory functions: Funções que criam e retornam outras funções com comportamento personalizado.
- Módulos: Padrão de módulo que encapsula dados privados e expõe uma API pública.
- Callbacks e event handlers: Closures permitem que callbacks acessem variáveis do escopo onde foram definidos.
- Funções de alta ordem: Como
Array.map,filter, etc., muitas vezes usam closures.
Exemplo de módulo com closure:
function criarModulo() {
let privado = "dado privado";
return {
getPrivado: function() {
return privado;
},
setPrivado: function(valor) {
privado = valor;
}
};
}
const modulo = criarModulo();
console.log(modulo.getPrivado()); // "dado privado"
modulo.setPrivado("novo valor");
console.log(modulo.getPrivado()); // "novo valor"Outro caso é a criação de funções com estado, como contadores ou acumuladores.
Pegadinhas
Closures podem causar comportamentos inesperados, especialmente em loops. O exemplo clássico:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// Imprime 5, 5, 5, 5, 5Aqui, todas as funções de callback compartilham a mesma variável i (com var), que após o loop vale 5. Para corrigir, usamos let (que cria um novo escopo a cada iteração) ou um closure adicional:
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// Imprime 0, 1, 2, 3, 4
// Ou com closure:
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 100);
})(i);
}Outra pegadinha é o vazamento de variáveis em closures que retêm referências a objetos grandes, causando memory leaks. É importante gerenciar o ciclo de vida das variáveis.
Boas práticas
- Prefira
leteconstavarpara evitar confusões de escopo. - Use closures para encapsulamento e criação de funções com estado.
- Evite closures desnecessárias que possam reter referências a objetos grandes.
- Em loops, utilize
letou IIFE para criar escopo por iteração.
Referências
- MDN: Closures
- MDN: let
- MDN: const
- MDN: Arrow functions
- JavaScript.info: Closures
- W3Schools: JavaScript Closures
Exercícios
Crie uma função
criarSaudacaoque recebe um nome e retorna uma função que, ao ser chamada, exibe "Olá, [nome]!". Teste com diferentes nomes.✓ Resposta:function criarSaudacao(nome) { return function() { console.log(`Olá, ${nome}!`); }; } const saudacaoJoao = criarSaudacao("João"); saudacaoJoao(); // Olá, João! const saudacaoMaria = criarSaudacao("Maria"); saudacaoMaria(); // Olá, Maria!Explique o que será impresso no console e por quê:
let x = 10; function foo() { console.log(x); } function bar() { let x = 20; foo(); } bar();✓ Resposta: Será impresso10. A funçãofoofoi definida no escopo global, então seu escopo léxico é o global, independentemente de onde é chamada. Portanto,xno console é a variável globalx(10), não a local debar.Escreva uma função
criarMultiplicadorque recebe um númerone retorna uma função que multiplica seu argumento porn.✓ Resposta:function criarMultiplicador(n) { return function(x) { return x * n; }; } const duplicar = criarMultiplicador(2); console.log(duplicar(5)); // 10 const triplicar = criarMultiplicador(3); console.log(triplicar(5)); // 15Corrija o código abaixo para que imprima 0, 1, 2, 3, 4 (cada um após 1 segundo):
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000); }✓ Resposta: Usandoletou IIFE. Exemplo comlet:
Ou com IIFE:for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000); }for (var i = 0; i < 5; i++) { (function(j) { setTimeout(function() { console.log(j); }, 1000); })(i); }Crie uma função
criarContadorComPassoque aceita um passo (incremento) e retorna um objeto com métodosincrementareobterValor. O contador deve começar em 0.✓ Resposta:function criarContadorComPasso(passo) { let valor = 0; return { incrementar: function() { valor += passo; }, obterValor: function() { return valor; } }; } const contador = criarContadorComPasso(5); contador.incrementar(); console.log(contador.obterValor()); // 5 contador.incrementar(); console.log(contador.obterValor()); // 10