Escopo: global, local, de bloco e closures

1. Introdução ao conceito de escopo em JavaScript

Escopo define onde variáveis e funções são acessíveis dentro do código. É um dos conceitos mais fundamentais do JavaScript, pois determina como o motor da linguagem resolve nomes de identificadores. Sem entender escopo, você inevitavelmente encontrará bugs misteriosos.

JavaScript utiliza escopo léxico (ou estático), onde o escopo de uma variável é determinado pela posição onde ela é declarada no código-fonte, durante a fase de compilação. Diferentemente do escopo dinâmico (usado em linguagens como Bash), o escopo léxico não depende da pilha de chamadas em tempo de execução.

A cadeia de escopos (scope chain) é o mecanismo que o motor do JavaScript usa para resolver variáveis: quando uma variável é referenciada, o motor busca primeiro no escopo atual, depois no escopo pai, e assim sucessivamente até chegar ao escopo global. Se não encontrar, lança um ReferenceError.

const global = 'estou no escopo global';

function externa() {
  const localExterna = 'escopo da função externa';

  function interna() {
    const localInterna = 'escopo da função interna';
    console.log(global);        // acessível via cadeia de escopos
    console.log(localExterna);  // acessível via cadeia de escopos
    console.log(localInterna);  // acessível (escopo atual)
  }

  interna();
  // console.log(localInterna); // ReferenceError: não acessível aqui
}

externa();

2. Escopo global: características e riscos

O escopo global é o escopo padrão quando você declara variáveis fora de qualquer função ou bloco. Em navegadores, o objeto global é window; no Node.js, é global.

// Escopo global
var variavelGlobalVar = 'acessível em todo lugar';
let variavelGlobalLet = 'também global, mas não no objeto global';
const constanteGlobal = 'imutável e global';

function exemplo() {
  console.log(variavelGlobalVar); // funciona
  console.log(variavelGlobalLet); // funciona
}

Problemas comuns:
- Poluição global: Múltiplos scripts podem sobrescrever variáveis globais acidentalmente.
- Colisão de nomes: Duas bibliotecas diferentes usando o mesmo nome global.
- Dificuldade de manutenção: Código difícil de testar e depurar.

Boas práticas:
- Em Node.js, cada módulo tem seu próprio escopo — variáveis declaradas no topo de um módulo não vazam para o global.
- Em React, evite variáveis globais dentro de componentes; prefira estado ou contexto.

// Em Node.js, isso não polui o global
const config = { api: 'https://api.exemplo.com' };
// config não está em global.config

3. Escopo local (de função): variáveis e parâmetros

Cada função em JavaScript cria seu próprio escopo. Variáveis declaradas dentro de uma função (com var, let ou const) e seus parâmetros ficam acessíveis apenas dentro dela.

function calcularPreco(precoBase, imposto) {
  // precoBase e imposto são variáveis locais (parâmetros)
  var precoFinal = precoBase * (1 + imposto); // var tem escopo de função

  if (precoFinal > 100) {
    var desconto = 0.1; // var "vaza" para o escopo da função
    let descontoLet = 0.15; // let fica restrito ao bloco
    console.log(descontoLet); // acessível aqui
  }

  console.log(desconto); // 0.1 (var vazou do if)
  // console.log(descontoLet); // ReferenceError
  return precoFinal;
}

Hoisting: Declarações com var são içadas (hoisted) para o topo do escopo da função, mas sem inicialização. let e const também sofrem hoisting, mas ficam na "zona morta temporal" (Temporal Dead Zone) até a declaração.

function exemploHoisting() {
  console.log(x); // undefined (hoisting de var)
  // console.log(y); // ReferenceError: TDZ
  var x = 10;
  let y = 20;
}

4. Escopo de bloco: let, const e a diferença do var

Blocos {} (usados em if, for, while) criam escopo apenas para let e const. var ignora blocos e respeita apenas escopo de função.

if (true) {
  var escopoVar = 'vaza do bloco';
  let escopoLet = 'preso no bloco';
  const escopoConst = 'também preso no bloco';
}

console.log(escopoVar); // 'vaza do bloco'
// console.log(escopoLet); // ReferenceError
// console.log(escopoConst); // ReferenceError

Problema clássico de closures com var em laços:

// Problema com var
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 3, 3, 3
}

// Solução com let (escopo de bloco)
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 100); // 0, 1, 2
}

Com var, todas as funções callback compartilham a mesma variável i (que já vale 3 quando os timeouts executam). Com let, cada iteração cria um novo escopo de bloco, capturando o valor correto.

5. Closures: capturando escopos de forma permanente

Uma closure é uma função que "lembra" do escopo onde foi criada, mesmo após esse escopo ter sido executado. É um dos recursos mais poderosos do JavaScript.

function criarContador() {
  let contador = 0; // variável privada

  return function() {
    contador++;
    return contador;
  };
}

const meuContador = criarContador();
console.log(meuContador()); // 1
console.log(meuContador()); // 2
console.log(meuContador()); // 3

A função retornada mantém uma referência ao escopo de criarContador, mantendo contador vivo na memória.

Closures em React:

function ContadorComponente({ incremento }) {
  const [contador, setContador] = useState(0);

  // Closure captura incremento e contador
  const handleClick = () => {
    setContador(prev => prev + incremento);
  };

  return <button onClick={handleClick}>Clique</button>;
}

6. Closures na prática: módulos e encapsulamento

Padrão IIFE (Immediately Invoked Function Expression) para criar escopo privado:

const modulo = (function() {
  let variavelPrivada = 0;

  function metodoPrivado() {
    console.log('acessível apenas internamente');
  }

  return {
    incrementar: function() {
      variavelPrivada++;
      metodoPrivado();
    },
    obterValor: function() {
      return variavelPrivada;
    }
  };
})();

modulo.incrementar();
console.log(modulo.obterValor()); // 1
// console.log(modulo.variavelPrivada); // undefined

Módulos ES6 gerenciam escopo automaticamente:

// arquivo: contador.js
let contador = 0; // privado ao módulo

export function incrementar() {
  contador++;
}

export function obter() {
  return contador;
}

CommonJS em Node.js:

// arquivo: logger.js
const nivel = 'info'; // privado ao módulo

module.exports = {
  log: (mensagem) => console.log(`[${nivel}] ${mensagem}`)
};

7. Escopo em React: componentes, hooks e reatividade

Em componentes funcionais, cada renderização cria um novo escopo. Isso pode causar stale closures (closures obsoletas) em hooks.

function Temporizador() {
  const [segundos, setSegundos] = useState(0);

  // PROBLEMA: stale closure
  useEffect(() => {
    const intervalo = setInterval(() => {
      setSegundos(segundos + 1); // segundos está "preso" na primeira renderização
    }, 1000);
    return () => clearInterval(intervalo);
  }, []); // eslint-disable-line

  return <div>{segundos} segundos</div>;
}

Soluções:

// Solução 1: dependências corretas
useEffect(() => {
  const intervalo = setInterval(() => {
    setSegundos(prev => prev + 1); // forma funcional
  }, 1000);
  return () => clearInterval(intervalo);
}, []);

// Solução 2: useRef para valores mutáveis
const segundosRef = useRef(segundos);
segundosRef.current = segundos;

useEffect(() => {
  const intervalo = setInterval(() => {
    console.log(segundosRef.current); // sempre o valor atual
  }, 1000);
  return () => clearInterval(intervalo);
}, []);

8. Depuração e boas práticas finais

Ferramentas de depuração:
- DevTools do navegador: Aba Sources → Scope mostra a cadeia de escopos atual.
- Node.js debugger: Use node inspect ou debugger; no código.
- console.log com contexto: console.log({ variavel }) para ver nome e valor.

Regras de ouro:
1. Prefira const por padrão, use let quando precisar reatribuir, evite var.
2. Minimize o escopo: declare variáveis o mais próximo possível de onde são usadas.
3. Evite globais: use módulos (ES6 ou CommonJS) para encapsulamento.
4. Cuidado com closures em loops: use let ou funções de fábrica.
5. Em React, respeite as dependências dos hooks: o ESLint plugin react-hooks/exhaustive-deps é seu amigo.

Checklist para evitar bugs de escopo:
- [ ] Variáveis globais são intencionais e mínimas?
- [ ] var foi substituído por let/const?
- [ ] Closures em loops usam let ou IIFE?
- [ ] Hooks React têm dependências corretas?
- [ ] Módulos Node.js não vazam variáveis para o escopo global?

Referências

  • MDN Web Docs: Escopo — Documentação oficial da Mozilla sobre escopo em JavaScript, com exemplos detalhados de escopo global, local e de bloco.
  • JavaScript.info: Closures — Tutorial completo e interativo sobre closures em JavaScript, com exercícios práticos e explicações visuais da cadeia de escopos.
  • React Documentation: Hooks and Closures — Seção oficial da documentação do React explicando como closures funcionam com hooks e como evitar stale closures.
  • Node.js Documentation: Modules — Documentação oficial do Node.js sobre o sistema de módulos CommonJS e como ele gerencia escopo automaticamente.
  • Exploring JS: Variable Scope — Capítulo do livro "Exploring JS" do Dr. Axel Rauschmayer sobre escopo de variáveis, hoisting e temporal dead zone.
  • W3Schools: JavaScript Scope — Tutorial introdutório sobre escopo em JavaScript com exemplos práticos de escopo global, local e de bloco.