Javascript

Escopo, Hoisting e Closures Já leu

9 min de leitura

Escopo, Hoisting e Closures
Este é um dos artigos mais importantes da série. Não porque seja o mais difícil — mas porque os conceitos aqui explicam c

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, let e const
  • A Temporal Dead Zone do let e const
  • 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 var em loops e como let resolve

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

Comentários

Mais em Javascript

O que é JavaScript e por que aprender?
O que é JavaScript e por que aprender?

Se voc&ecirc; quer trabalhar com desenvolvimento web &mdash; seja criando sit...

Trabalhando com JSON
Trabalhando com JSON

JSON est&aacute; em absolutamente tudo. &Eacute; o formato de dados mais usad...

Dominando o JavaScript
Dominando o JavaScript

Estou estudando Javascript a um longo tempo. N&atilde;o sei precisar quanto t...