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.
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
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);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:
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));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:
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'));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:
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]));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:
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);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:
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);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:
function aplicarCupom(total) {
desconto = 15;
return total - desconto;
}
console.log(aplicarCupom(200));
console.log('vazou para o global?', globalThis.desconto);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:
'use strict';
function aplicarCupom(total) {
desconto = 15;
return total - desconto;
}
console.log(aplicarCupom(200));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:
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);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:
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);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
letno 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ê
MOEDAdo escopo de fora só funciona naquele arquivo; a mesma função recebendomoedacomo parâmetro é testável e reutilizável. - Bloco com muitos nomes é uma função disfarçada. Se o corpo de um
ifdeclara 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.
Perguntas frequentes
Escopo é decidido onde a função foi escrita ou onde ela é chamada?
Posso declarar duas variáveis com o mesmo nome em escopos diferentes?
Módulo ES precisa de use strict?
Por que meu console.log dentro do if funciona e o de fora não?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- MDN — Escopo — developer.mozilla.org
- MDN — Strict mode — developer.mozilla.org
- ECMAScript 2026 — Executable Code and Execution Contexts — tc39.es


