Este é um dos artigos mais importantes da série. Não porque seja o mais difícil — mas porque os conceitos aqui explicam como o JavaScript realmente funciona por baixo dos panos. Muitos bugs misteriosos, comportamentos inesperados e confusões com variáveis têm raiz exatamente nestes três temas: escopo, hoisting e closures.
Desenvolvedores que entendem isso escrevem código mais previsível, mais seguro e mais fácil de depurar. Vamos com calma.
Escopo — onde uma variável existe
Escopo é o contexto em que uma variável existe e pode ser acessada. Pense como se fosse um conjunto de paredes invisíveis ao redor do seu código.
Escopo Global
Uma variável declarada fora de qualquer função ou bloco tem escopo global — ela existe em todo o programa:
const mensagem = "Olá, mundo!"; // escopo global
function exibir() {
console.log(mensagem); // acessa sem problema
}
exibir(); // Olá, mundo!
console.log(mensagem); // Olá, mundo!
Evite variáveis globais sempre que possível. Elas podem ser alteradas por qualquer parte do código, causando bugs difíceis de rastrear.
Escopo de Função
Variáveis declaradas dentro de uma função existem apenas dentro dela:
function calcular() {
const resultado = 42; // escopo local da função
console.log(resultado); // 42
}
calcular();
console.log(resultado); // ReferenceError: resultado is not defined
Cada chamada de função cria seu próprio escopo isolado — suas variáveis não vazam para fora.
Escopo de Bloco
Com let e const, variáveis também ficam restritas ao bloco {} em que foram declaradas:
if (true) {
let x = 10;
const y = 20;
console.log(x); // 10
console.log(y); // 20
}
console.log(x); // ReferenceError: x is not defined
console.log(y); // ReferenceError: y is not defined
Isso não acontece com var — e é exatamente por isso que var é problemático:
if (true) {
var z = 30; // var ignora o escopo de bloco!
}
console.log(z); // 30 ← vaza para fora do bloco
Essa é uma das principais razões para nunca usar var em código moderno.
Cadeia de Escopos (Scope Chain)
Quando o JavaScript não encontra uma variável no escopo atual, ele sobe na cadeia de escopos procurando no escopo pai:
const idioma = "Português"; // escopo global
function configurar() {
const tema = "escuro"; // escopo de configurar
function exibirConfig() {
// acessa variáveis do próprio escopo, do pai e do global
console.log(idioma); // Português ← do escopo global
console.log(tema); // escuro ← do escopo pai
}
exibirConfig();
}
configurar();
A busca vai sempre de dentro para fora — nunca de fora para dentro.
Hoisting — elevação de declarações
Hoisting é o comportamento do JavaScript de "elevar" declarações para o topo do seu escopo antes da execução. É como se o JavaScript fizesse uma passagem pelo código antes de rodá-lo, registrando todas as declarações.
Hoisting com function declaration
Funções declaradas com function sofrem hoisting completo — você pode chamá-las antes de declará-las:
// Chamada ANTES da declaração — funciona!
console.log(somar(3, 4)); // 7
function somar(a, b) {
return a + b;
}
O JavaScript eleva a função inteira para o topo do escopo antes de executar qualquer linha.
Hoisting com var
Variáveis declaradas com var têm sua declaração elevada, mas não sua inicialização:
console.log(nome); // undefined (não dá erro, mas não tem valor)
var nome = "Ana";
console.log(nome); // Ana
O que o JavaScript efetivamente enxerga:
var nome; // declaração elevada
console.log(nome); // undefined
nome = "Ana"; // inicialização fica no lugar
console.log(nome); // Ana
Isso é confuso e imprevisível — mais um motivo para evitar var.
Hoisting com let e const
let e const também são elevados, mas ficam em uma Temporal Dead Zone (TDZ) — não podem ser acessados antes da declaração:
console.log(cidade); // ReferenceError: Cannot access 'cidade' before initialization
const cidade = "São Paulo";
Este erro é muito melhor do que o undefined silencioso do var. Ele avisa imediatamente que algo está errado.
Hoisting com function expression e arrow function
Expressões de função e arrow functions não sofrem hoisting:
// Erro! multiplicar ainda não foi inicializada
console.log(multiplicar(3, 4)); // TypeError
const multiplicar = (a, b) => a * b;
Isso reforça a boa prática de declarar antes de usar.
Closures — funções que lembram
Closure é o conceito mais poderoso desta aula. Uma closure é criada quando uma função lembra do escopo em que foi criada, mesmo depois que esse escopo não existe mais.
Vamos ver passo a passo:
function criarContador() {
let contagem = 0; // variável do escopo de criarContador
function incrementar() {
contagem++; // acessa e modifica a variável do escopo pai
console.log(contagem);
}
return incrementar; // retorna a função — não a chama!
}
const contador = criarContador();
// criarContador() terminou de executar
// mas a variável "contagem" continua viva dentro da closure
contador(); // 1
contador(); // 2
contador(); // 3
criarContador() já terminou sua execução, mas contagem continua existindo porque incrementar a referencia. Isso é uma closure.
Closures na prática — fábricas de funções
Closures permitem criar funções especializadas a partir de funções genéricas:
function criarMultiplicador(fator) {
return (numero) => numero * fator;
}
const dobrar = criarMultiplicador(2);
const triplicar = criarMultiplicador(3);
const decuplicar = criarMultiplicador(10);
console.log(dobrar(5)); // 10
console.log(triplicar(5)); // 15
console.log(decuplicar(5)); // 50
Cada função criada tem sua própria closure com um fator diferente.
Closures para dados privados
Uma das aplicações mais elegantes de closures é simular dados privados — variáveis que só podem ser acessadas por funções específicas:
function criarContaBancaria(saldoInicial) {
let saldo = saldoInicial; // privado — ninguém acessa diretamente
return {
depositar(valor) {
if (valor <= 0) return console.log("Valor inválido.");
saldo += valor;
console.log(`Depositado R$ ${valor}. Saldo: R$ ${saldo}`);
},
sacar(valor) {
if (valor > saldo) return console.log("Saldo insuficiente.");
saldo -= valor;
console.log(`Sacado R$ ${valor}. Saldo: R$ ${saldo}`);
},
verSaldo() {
console.log(`Saldo atual: R$ ${saldo}`);
},
};
}
const minhaConta = criarContaBancaria(1000);
minhaConta.verSaldo(); // Saldo atual: R$ 1000
minhaConta.depositar(500); // Depositado R$ 500. Saldo: R$ 1500
minhaConta.sacar(200); // Sacado R$ 200. Saldo: R$ 1300
// Tentativa de acesso direto — impossível
console.log(minhaConta.saldo); // undefined — protegido pela closure!
O bug clássico de closure com var em loops
Este é um dos erros mais famosos do JavaScript — entendê-lo solidifica seu conhecimento de closure e escopo:
// ❌ Comportamento inesperado com var
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// Após 1 segundo: 4, 4, 4 ← não é o que esperávamos!
Por quê? var não tem escopo de bloco — todas as callbacks compartilham a mesma variável i, que já vale 4 quando o timeout executa.
// ✅ Correto com let — cada iteração tem seu próprio escopo
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// Após 1 segundo: 1, 2, 3 ← correto!
let cria uma nova variável i para cada iteração do loop — cada closure captura o seu próprio valor.
Tudo junto — um exemplo real
function criarPlacar(nomeTime) {
let pontos = 0;
let jogos = 0;
return {
vencer() {
pontos += 3;
jogos++;
console.log(`${nomeTime} venceu! ${pontos} pontos em ${jogos} jogos.`);
},
empatar() {
pontos += 1;
jogos++;
console.log(`${nomeTime} empatou. ${pontos} pontos em ${jogos} jogos.`);
},
perder() {
jogos++;
console.log(`${nomeTime} perdeu. ${pontos} pontos em ${jogos} jogos.`);
},
status() {
const media = jogos > 0 ? (pontos / jogos).toFixed(1) : 0;
console.log(`${nomeTime}: ${pontos}pts | ${jogos} jogos | Média: ${media}pts/jogo`);
},
};
}
const time = criarPlacar("Grêmio");
time.vencer(); // Grêmio venceu! 3 pontos em 1 jogos.
time.vencer(); // Grêmio venceu! 6 pontos em 2 jogos.
time.empatar(); // Grêmio empatou. 7 pontos em 3 jogos.
time.perder(); // Grêmio perdeu. 7 pontos em 4 jogos.
time.status(); // Grêmio: 7pts | 4 jogos | Média: 1.8pts/jogo
Tarefa para você
Desafio 1: Crie uma função criarSerie que receba o título de uma série e retorne um objeto com métodos para adicionarEpisodio, marcarAssistido e progresso:
const serie = criarSerie("Breaking Bad");
serie.adicionarEpisodio(); // total: 1
serie.adicionarEpisodio(); // total: 2
serie.marcarAssistido(); // assistidos: 1
serie.progresso(); // "Breaking Bad: 1/2 episódios assistidos (50%)"
Desafio 2: Explique com suas palavras por que o código abaixo imprime 3, 3, 3 e como corrigir:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 500);
}
Conclusão
Neste artigo você aprendeu:
- Escopo global, de função e de bloco
- A cadeia de escopos e como o JS busca variáveis
- Hoisting de funções,
var,leteconst - A Temporal Dead Zone do
leteconst - O conceito de closure e como funções lembram seu escopo
- Fábricas de funções e dados privados com closures
- O bug clássico de
varem loops e comoletresolve
No próximo artigo fechamos o Módulo 1 com tratamento de erros usando try, catch e finally — essencial para escrever código robusto que não quebra na primeira adversidade.
📚 Fontes e Referências
- MDN Web Docs — Closures: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Closures
- MDN Web Docs — Hoisting: https://developer.mozilla.org/pt-BR/docs/Glossary/Hoisting
- MDN Web Docs — Escopo: https://developer.mozilla.org/pt-BR/docs/Glossary/Scope
- JavaScript.info — Variable Scope and Closure: https://javascript.info/closure
- JavaScript.info — The old "var": https://javascript.info/var
- You Don't Know JS: Scope & Closures — Kyle Simpson: https://github.com/getify/You-Dont-Know-JS
- Eloquent JavaScript, Cap. 3 — Functions: https://eloquentjavascript.net/03_functions.html
- JavaScript: The Good Parts — Douglas Crockford (O'Reilly)