Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

Escopo em JavaScript: global, de função e de bloco

Escopo léxico, cadeia de escopos, sombreamento e a variável global criada sem querer — os três níveis com código executado no Node e o erro de cada um.

Rodolfo Mori7 min de leitura

Escopo é a região do código em que um identificador pode ser resolvido. Em JavaScript há escopo global, de função e de bloco. let e const respeitam as chaves de um bloco; var atravessa essas chaves e fica limitado pela função ou pelo ambiente global.

Imagine salas dentro de uma casa. De uma sala interna você consegue consultar o que está nela e, depois, os ambientes que a envolvem. Do corredor externo você não enxerga a gaveta criada dentro do quarto. O mecanismo técnico segue essa busca: o motor procura o nome no ambiente atual e sobe a cadeia de escopos até encontrar a primeira declaração. Se não encontrar, lança ReferenceError.

Essa organização é léxica: as paredes são definidas pelo lugar em que a função foi escrita no código, não por quem a chamou durante a execução.

Esta lição aprofunda o que a lição de variáveis em JavaScript apresentou: lá o assunto era qual palavra usar; aqui é até onde o nome alcança.

Três escopos, uma regra

js
const MOEDA = 'BRL';

function montarPedido(cliente) {
  const id = 7412;

  if (cliente.vip) {
    const desconto = 0.1;
    console.log('desconto vip:', desconto);
  }

  return `${MOEDA} · pedido ${id} de ${cliente.nome}`;
}

console.log(montarPedido({ nome: 'Ana Souza', vip: true }));
console.log('id fora da função?', typeof id);
console.log('desconto fora do if?', typeof desconto);
desconto vip: 0.1 BRL · pedido 7412 de Ana Souza id fora da função? undefined desconto fora do if? undefined

MOEDA é visível de qualquer lugar do arquivo. id existe só dentro de montarPedido. desconto existe só dentro do if. Cada nível enxerga tudo que está acima dele e nada do que está abaixo — a visibilidade é uma via de mão única, de dentro para fora.

O typeof foi usado de propósito no lugar do acesso direto: ele é o único operador que não lança erro com nome inexistente. Acessar id diretamente ali derrubaria o programa, como você vai ver na seção de erros.

Escopo léxico: vale onde a função foi escrita

Esta é a parte que quase todo material trata rápido demais, e é a que decide o comportamento. Veja duas variáveis taxa e adivinhe a saída antes de ler:

js
const taxa = 0.05;

function calcularTaxa(valor) {
  return valor * taxa;
}

function cobrar(valor) {
  const taxa = 0.5;
  return calcularTaxa(valor);
}

console.log(cobrar(1000));
console.log(calcularTaxa(1000));
50 50

cobrar declarou taxa = 0.5 e chamou calcularTaxa logo em seguida. Mesmo assim o resultado foi 50, e não 500: calcularTaxa foi escrita ao lado do const taxa = 0.05 do topo do arquivo, e é esse o taxa que ela enxerga para sempre. A taxa de cobrar é uma variável diferente que por acaso tem o mesmo nome.

Isso se chama escopo léxico, ou estático: dá para determinar quem é cada nome só lendo o código, sem executar nada. O contrário — resolver o nome pela pilha de chamadas — chama-se escopo dinâmico e existe em poucas linguagens. Em JavaScript só o this se comporta assim, e é justamente por isso que this é um capítulo à parte.

A cadeia de escopos

Quando uma função está dentro de outra, os escopos se encaixam. O motor sobe degrau por degrau até achar o nome:

js
const MOEDA = 'BRL';

function montarNota(cliente) {
  const id = 7412;

  function formatar(valor) {
    const simbolo = MOEDA === 'BRL' ? 'R$' : '$';
    return `[${id}/${cliente}] ${simbolo} ${valor.toFixed(2)}`;
  }

  return formatar(479.7);
}

console.log(montarNota('ana-souza'));
[7412/ana-souza] R$ 479.70

formatar usa quatro nomes de três níveis diferentes: simbolo é dela, id e cliente vêm de montarNota, MOEDA vem do módulo. Nenhum deles precisou ser passado como parâmetro — a função enxerga tudo que envolve o lugar onde ela foi escrita.

Essa é a mecânica que sustenta closure: se montarNota devolvesse formatar em vez de chamá-la, a função continuaria enxergando id e cliente mesmo depois de montarNota ter terminado.

escopo criado por let / const var function declaration
módulo o arquivo .mjs fica no módulo fica no módulo fica no módulo
função function fica na função fica na função fica na função
bloco { }, if, for fica no bloco vaza para a função fica no bloco

var ignora o bloco

A coluna que vaza na tabela acima merece uma demonstração. Variáveis declaradas com var dentro de um laço continuam vivas depois dele:

js
function fecharCaixa(vendas) {
  for (var i = 0; i < vendas.length; i += 1) {
    var ultima = vendas[i];
  }
  return { i, ultima };
}

console.log(fecharCaixa([120, 340, 89]));
{ i: 3, ultima: 89 }

Tanto o contador quanto a variável do corpo sobreviveram ao for e estavam disponíveis no return. Parece conveniente e é a origem de uma classe inteira de bug: dois laços no mesmo método reaproveitando i sem querer, ou uma variável de dentro de um if sendo lida em outro ramo onde ela nunca foi atribuída.

Trocar var por let no mesmo código gera ReferenceError na linha do return. Esse erro é uma correção, não um obstáculo: ele aponta para o lugar exato onde a suposição estava errada.

Sombreamento: o mesmo nome em dois níveis

Declarar um nome que já existe num escopo de fora é permitido. O de dentro esconde o de fora dentro daquele bloco:

js
const desconto = 5;

function aplicar(total) {
  const desconto = 20;

  if (total > 500) {
    const desconto = 50;
    return total - desconto;
  }

  return total - desconto;
}

console.log(aplicar(1000), aplicar(300), desconto);
950 280 5

Três variáveis chamadas desconto, em três níveis, com três valores. Nenhuma delas apagou a outra: o desconto do módulo continua valendo 5 depois de tudo. O sombreamento é uma ferramenta legítima — é o que permite chamar de item o parâmetro de todo callback sem pensar duas vezes.

Ele vira problema quando é acidental. Se você leu este bloco duas vezes para ter certeza de qual desconto cada return usou, imagine em um arquivo de 300 linhas. Nome repetido em níveis diferentes só compensa quando os dois são obviamente a mesma coisa.

O escopo global e a poluição que ele causa

Em módulo ES — todo arquivo .mjs, e todo <script type="module"> — nada declarado no topo vai para o objeto global:

js
const taxaInterna = 0.18;
var tambemInterna = 'nem var escapa';

console.log('taxaInterna no globalThis?', 'taxaInterna' in globalThis);
console.log('tambemInterna no globalThis?', 'tambemInterna' in globalThis);
console.log('this no topo do módulo:', this);
taxaInterna no globalThis? false tambemInterna no globalThis? false this no topo do módulo: undefined

Nem mesmo var escapa: o topo de um módulo é um escopo próprio, não o global. É por isso que dois módulos podem declarar total sem se atropelar, e por isso que a IIFE deixou de ser necessária.

Em script clássico a história é outra: var no topo vira propriedade de window, e é assim que duas bibliotecas antigas brigam pela variável $.

use strict e a variável criada sem querer

O modo relaxado do JavaScript tem um comportamento que hoje seria considerado um bug de linguagem: atribuir a um nome nunca declarado cria uma variável global. Em CommonJS sem use strict:

js
function aplicarCupom(total) {
  desconto = 15;
  return total - desconto;
}

console.log(aplicarCupom(200));
console.log('vazou para o global?', globalThis.desconto);
185 vazou para o global? 15

Faltou um const na linha 2, e o resultado foi uma variável global chamada desconto, visível por todo o processo. O código funcionou — e é justamente por funcionar que o defeito sobrevive até virar conflito com outro arquivo.

Com a diretiva no topo, o mesmo código para de passar:

js
'use strict';

function aplicarCupom(total) {
  desconto = 15;
  return total - desconto;
}

console.log(aplicarCupom(200));
/private/tmp/loja/escopo-strict.cjs:4 desconto = 15; ^

ReferenceError: desconto is not defined at aplicarCupom (/private/tmp/loja/escopo-strict.cjs:4:12) at Object.<anonymous> (/private/tmp/loja/escopo-strict.cjs:8:13) at Module._compile (node:internal/modules/cjs/loader:1854:14)

Node.js v24.16.0

Note que o caminho no rastro não tem file:// na frente, ao contrário de todos os outros erros desta trilha: é assim que se reconhece de olho um erro de CommonJS. Se você escreve módulos ES, esse comportamento já está desligado para você — o modo estrito é obrigatório neles.

Erros comuns: usar o nome depois que o bloco fechou

O erro característico do escopo de bloco não avisa nada até a linha exata em que você tenta usar o nome:

js
const precos = [19.9, 49.9, 129.9];

for (let i = 0; i < precos.length; i += 1) {
  const linha = `item ${i + 1}: R$ ${precos[i].toFixed(2)}`;
  console.log(linha);
}

console.log('última linha:', linha);
item 1: R$ 19.90 item 2: R$ 49.90 item 3: R$ 129.90 file:///private/tmp/loja/escopo-loop-erro.mjs:8 console.log('última linha:', linha); ^

ReferenceError: linha is not defined at file:///private/tmp/loja/escopo-loop-erro.mjs:8:30 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)

Node.js v24.16.0

O laço rodou inteiro e imprimiu as três linhas antes de quebrar. Isso engana: como o console.log de dentro funcionou, a primeira reação é achar que a variável existe. Ela existiu — três vezes, uma por volta do laço — e foi destruída em cada fechamento de chave.

A mensagem é is not defined, e não Cannot access ... before initialization. A diferença importa: a primeira quer dizer que o nome não existe naquele escopo; a segunda, que existe mas você chegou cedo demais. A segunda é assunto de hoisting em JavaScript.

A correção depende do que você quer. Se precisa do último valor, declare fora:

js
const precos = [19.9, 49.9, 129.9];
let ultimaLinha = '';

for (let i = 0; i < precos.length; i += 1) {
  ultimaLinha = `item ${i + 1}: R$ ${precos[i].toFixed(2)}`;
  console.log(ultimaLinha);
}

console.log('última linha:', ultimaLinha);
item 1: R$ 19.90 item 2: R$ 49.90 item 3: R$ 129.90 última linha: item 3: R$ 129.90

Como organizar escopo num projeto real

Escopo bem desenhado é o que faz um arquivo caber na cabeça. Quatro decisões que funcionam:

  • Declare no menor escopo possível e só suba quando precisar. É mais fácil mover uma variável para fora depois do que descobrir quem a alterou.
  • Não use o escopo de módulo como depósito de estado. Um let no topo do arquivo alterado por três funções diferentes é uma variável global com outro nome.
  • Parâmetro é melhor que captura. Uma função que lê MOEDA do escopo de fora só funciona naquele arquivo; a mesma função recebendo moeda como parâmetro é testável e reutilizável.
  • Bloco com muitos nomes é uma função disfarçada. Se o corpo de um if declara cinco variáveis, extraia — e o escopo vira parte da assinatura.

Desenhe o caminho de uma variável: declare taxa no módulo, outra taxa dentro de uma função e uma terceira dentro de um if. Imprima em cada nível e, antes de rodar, escreva qual declaração será encontrada. Depois tente acessar a taxa do bloco do lado de fora e confirme o ReferenceError. Se a previsão bateu, você aplicou a busca de dentro para fora e o sombreamento.

A próxima lição desta trilha mostra o que acontece antes de a primeira linha rodar, e por que var, function, let e class chegam ao escopo em estados diferentes: hoisting em JavaScript. O guia completo de JavaScript tem a trilha inteira em ordem, e a categoria de front-end reúne o resto do conteúdo de navegador.

  • escopo
  • escopo lexico
  • use strict
  • bloco
  • global

Perguntas frequentes

Escopo é decidido onde a função foi escrita ou onde ela é chamada?
Onde foi escrita. JavaScript tem escopo léxico: o motor resolve os nomes olhando os blocos que envolvem a função no código-fonte, não a pilha de chamadas. Uma variável local de quem chamou é invisível para quem foi chamado, mesmo com o mesmo nome. O this é a exceção — esse depende da chamada.
Posso declarar duas variáveis com o mesmo nome em escopos diferentes?
Pode, e isso se chama sombreamento. Funciona e às vezes é desejável, mas esconde a variável de fora dentro daquele bloco. Se você precisou sombrear para não pensar num nome melhor, provavelmente há uma função querendo nascer ali.
Módulo ES precisa de use strict?
Não. Todo módulo ES já roda em modo estrito, e nada declarado nele escapa para o objeto global. A diretiva só faz diferença em script solto e em arquivo CommonJS, onde o modo relaxado ainda é o padrão.
Por que meu console.log dentro do if funciona e o de fora não?
Porque let e const criam a variável dentro das chaves e a destroem quando elas fecham. Se você precisa do valor fora, declare fora e atribua dentro — ou faça o bloco devolver o valor, que costuma resultar num código melhor.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — Escopo — developer.mozilla.org
  2. MDN — Strict mode — developer.mozilla.org
  3. ECMAScript 2026 — Executable Code and Execution Contexts — tc39.es

Continue por aqui