Dia 2 — Variáveis, let e const

Informatica · Conteudo · publicado em 05/10/2026
Dia 2 de 13

Variáveis, let e const

Aula 1

let, const e o fim de var

let, const e o fim de var

var é a forma antiga de declarar variável e não deve aparecer em código novo.

let e const substituíram var porque var tem duas propriedades que

produzem erro sem mensagem: a declaração sobe até o topo do escopo (hoisting)

e ela vale para o programa inteiro, não para o bloco onde foi escrita. As duas

somem com let e const.

let total = 0;      // o valor pode mudar
const taxa = 0.15;  // o valor nao muda

let é para o que muda; const é para o que não muda. Começar por const e

trocar para let no momento em que a reatribuição aparecer é o caminho mais

simples — o compilador avisa quando o const está sendo reatribuído, e o aviso

aparece antes do bug.

A diferença prática aparece no exemplo da página logo nos dois primeiros logs.

total é declarado com let e vale 0; a linha seguinte soma 10 nele e o

console passa a responder 10, sem reclamar. taxa é declarado com const, e

tentar escrever nele não funciona. O mesmo arquivo, as duas palavras lado a lado:

uma que aceita a escrita e uma que não.

Declarar, atribuir e nomear

Uma variável é um nome que aponta para um valor. Declarar é criar esse

nome pela primeira vez; atribuir é escrever nele depois.

let contador;              // declarada, sem valor: vale undefined
contador = 1;              // atribuida
contador = contador + 1;   // atribuida de novo

O nome de variável é o identificador, e as regras do nome são poucas: começa com letra,

_ ou $; pode conter números depois do primeiro caractere; não pode ser

palavra reservada. O estilo do JavaScript é camelCase — nomeDoUsuario,

totalDeTarefas, idDaTarefa — porque o nome inteiro não aceita o hífen, que o

JavaScript leria como subtração.

O exemplo mostra a const por omissão no fim do arquivo: config é um objeto

com host e porta, e a linha imprime porta: 3000 e, ao lado,

campo ausente: undefined. Ler config.ausente não dá erro — o JavaScript

responde undefined para qualquer propriedade que o objeto não tem. É o mesmo

comportamento do contador declarado sem valor: a pergunta "isto existe?" e a

resposta undefined são a mesma coisa.

O arquivo .js não tem imutável por padrão: a tipagem dinâmica significa que o mesmo nome pode guardar tipos diferentes

em momentos diferentes, sem declarar nada: let valor = 1 e depois

valor = 'texto' é código legal. O typeof mostra qual é o tipo atual.

O exemplo confirma isso com duas linhas: valor inicial: 1 number e

mesmo nome, outro tipo: texto string. Nenhuma declaração de tipo apareceu entre

uma linha e outra — só a escrita, e o typeof saiu diferente.

O que o let proíbe e o var permitia

Redeclarar o mesmo nome com let no mesmo escopo é SyntaxError, e o erro

aparece antes de qualquer linha rodar — o programa nem chega a executar. Com

var, redeclarar é permitido e o segundo var sobrescreve o valor do primeiro.

try {
  eval('let a = 1; let a = 2;');
} catch (erro) {
  console.log(erro.constructor.name); // SyntaxError
}

O exemplo faz exatamente isso e imprime `redeclarar com let: SyntaxError -

Identifier 'a' has already been declared`. O nome do erro vem de

erro.constructor.name, e o texto vem de erro.message — são as duas coisas

que o objeto de erro entrega, e é assim que se imprime um erro com detalhe em

vez de undefined. Na sequência, o mesmo arquivo declara var v = 1 e depois

var v = 2, e o console responde redeclarar com var e permitido: 2. O segundo

declarado passou por cima do primeiro sem nada reclamar.

A diferença entre os dois erros é o momento. O SyntaxError do let acontece

quando o arquivo é lido, antes da primeira linha rodar — é por isso que ele

precisa de eval para aparecer no meio de um programa: um SyntaxError escrito

no arquivo impediria o arquivo inteiro de carregar. Já o erro de reatribuir

const é um TypeError em tempo de execução, e o exemplo o mostra com

erro ao reatribuir const: TypeError - Assignment to constant variable. Ele

precisa de try/catch pelo mesmo motivo: sem o try, a exceção derrubaria o

programa e nenhuma linha depois impressa.

Escopo de bloco: as chaves importam

Um escopo de bloco é o que está entre { }. let e const vivem só ali;

var vive na função inteira. A diferença aparece assim:

let fora = 'global';
{
  let fora = 'do bloco';
  console.log(fora);   // do bloco
}
console.log(fora);     // global

Com var o nome do bloco seria o mesmo nome da função, e a atribuição interna

vazeria para fora. Por isso let e const são o padrão: cada { } cria uma

barreira, e o código dentro dele não toca no código de fora.

O exemplo da página mostra a diferença completa, e mostra o lado bom e o lado

ruim. Com let, o console imprime dentro do bloco: do bloco e, depois da

chave, depois do bloco: global — o nome de fora continua valendo o que valia.

Com var a resposta é outra: a função devolve `var sobrevive ao bloco: veio do

var, porque o var declarado dentro do if` continua existindo quando a função

termina. E com let a mesma função tenta devolver o nome de dentro e recebe

let sobrevive ao bloco: ReferenceError: dentro is not defined.

Esse ReferenceError é a diferença que importa. O var vaza para fora e pode

ser usado sem ninguém perceber; o let dá erro na hora. Um erro na hora é

melhor que um vazamento silencioso: o vazamento aparece como bug em outra parte

do código, longe da linha que causou.

Quando o var ainda aparece

Código antigo, e biblioteca antiga. Em código novo, var não tem defesa: var

não tem escopo de bloco, não protege contra redeclaração e não avisa quando é

usado por engano. Em arquivos .js o JavaScript roda em modo frouxo

(sloppy mode), e nesse modo o erro de reatribuir const não interrompe a

execução — ele vira apenas um aviso. A const por omissão é o detalhe

relacionado: um objeto escrito com chaves e sem valores tem todos os valores

undefined.

Vale separar duas coisas que costumam vir juntas na mesma frase. O aviso de

reatribuir const vem do modo frouxo e não derruba o programa; o erro de

redeclarar let é SyntaxError e derruba independente do modo. E o

Object.freeze, que é a aula seguinte, é a forma de escrever código que roda

igual nos dois modos — assunto da próxima página, não deste parágrafo.

O que decide na prática é o arquivo novo. Um var que aparece hoje no seu

código significa que ninguém revisou aquela linha, e o conserto é mecânico:

trocar por let e, se o compilador reclamar na reatribuição, trocar por

const.

Exemplo

// let e const: o que cada um garante. O exemplo mostra a diferenca de
// escopo de bloco e o que o var deixava passar.
let total = 0;          // muda
const taxa = 0.15;      // nao muda: nem o codigo pode reatribuir
console.log('total:', total, '| taxa:', taxa);

total = total + 10;
console.log('depois de atribuir:', total);

// reatribuir const e TypeError. O aviso do console.error vai para o fluxo
// de erro; o console.log repete no fluxo normal.
try {
  taxa = 0.2;
} catch (erro) {
  console.error(erro.constructor.name + ': ' + erro.message);
  console.log('erro ao reatribuir const:', erro.constructor.name, '-', erro.message);
}

// declaracao sem valor vale undefined
let contador;
console.log('declarado sem valor:', contador, '| typeof:', typeof contador);
contador = 1;
console.log('apos atribuir:', contador);

// tipagem dinamica: o mesmo nome pode guardar tipos diferentes
let valor = 1;
console.log('valor inicial:', valor, typeof valor);
valor = 'texto';
console.log('mesmo nome, outro tipo:', valor, typeof valor);

// escopo de bloco: let vive dentro das chaves
let fora = 'global';
{
  let fora = 'do bloco';
  console.log('dentro do bloco:', fora);
}
console.log('depois do bloco:', fora);

// var nao tem escopo de bloco: ele sobrevive ao {}
function comVar() {
  if (true) { var dentro = 'veio do var'; }
  return dentro;
}
function comLet() {
  if (true) { let dentro = 'veio do let'; }
  try { return dentro; } catch (erro) { return erro.constructor.name + ': ' + erro.message; }
}
console.log('var sobrevive ao bloco:', comVar());
console.log('let sobrevive ao bloco:', comLet());

// redeclarar com let e SyntaxError, e o erro vem antes de qualquer execucao
try {
  eval('let a = 1; let a = 2;');
} catch (erro) {
  console.log('redeclarar com let:', erro.constructor.name, '-', erro.message);
}
var v = 1;
var v = 2;
console.log('redeclarar com var e permitido:', v);

// const por omissao: objeto com chave e sem valor tem undefined em tudo
const config = { host: 'localhost', porta: 3000 };
console.log('porta:', config.porta, '| campo ausente:', config.ausente);

Saída real

total: 0 | taxa: 0.15
depois de atribuir: 10
erro ao reatribuir const: TypeError - Assignment to constant variable.
declarado sem valor: undefined | typeof: undefined
apos atribuir: 1
valor inicial: 1 number
mesmo nome, outro tipo: texto string
dentro do bloco: do bloco
depois do bloco: global
var sobrevive ao bloco: veio do var
let sobrevive ao bloco: ReferenceError: dentro is not defined
redeclarar com let: SyntaxError - Identifier 'a' has already been declared
redeclarar com var e permitido: 2
porta: 3000 | campo ausente: undefined
Aula 2

const, escopo de bloco e o que não pode mudar

const, escopo de bloco e o que não pode mudar

const não significa imutável. Significa que o nome não pode ser reatribuído.

A diferença entre o nome e o conteúdo é o que faz muita gente errar: um const

que aponta para um objeto continua permitindo mudar as propriedades desse

objeto, porque o objeto continua o mesmo.

const tarefa = { texto: 'estudar', feita: false };
tarefa.feita = true;        // permitido: mudou o objeto
// tarefa = { texto: 'outra' };  // TypeError: mudou o nome

A diferença entre const e let, o const versus let, é esta: o que a reatribuição proíbe é escrever no nome. Por isso o padrão do material

é const no começo, e let só onde o valor realmente muda — tipicamente uma

variável de contagem ou de acumulador dentro de uma função.

O exemplo da página é uma demonstração em três tempos, e cada tempo produz uma

linha diferente no terminal. Primeiro o conteúdo muda: tarefa.feita = true e o

console imprime objeto: { texto: 'estudar', feita: true }. Depois o nome é

reatribuído e vem erro: TypeError - Assignment to constant variable. São duas

linhas sobre o mesmo objeto, e a diferença entre elas é tudo o que esta seção

afirma.

const com objeto e const com array

Os dois casos são o mesmo caso de conteúdo mutável, e vale a pena ver os dois:

const usuario = { nome: 'Ana', idade: 30 };
usuario.idade = 31;   // ok: o conteudo mudou
usuario = {};         // TypeError: o nome nao pode receber outro objeto

const tarefas = ['estudar'];
tarefas.push('treinar');   // ok: o conteudo mudou
tarefas = [];              // TypeError: o nome nao pode receber outro array

No array o exemplo repete exatamente o roteiro do objeto: o push de

'treinar' é aceito e a linha seguinte imprime `array: [ 'estudar', 'treinar'

]`, com os dois itens. A tentativa de trocar a referência sai como

erro ao reatribuir o array: TypeError - Assignment to constant variable. — a

mesma mensagem do objeto, palavra por palavra. É o esperado: o const não sabe

o que está apontando, e a regra é a mesma para qualquer valor.

O nome aponta para a caixa, e não para o conteúdo da caixa. Trocar o

conteúdo é mexer dentro dela; trocar a caixa é reatribuir. Por isso

const tarefas = [] seguido de tarefas.push(...) é o padrão do material: o

nome aponta para um array vazio e cresce dentro dele, e nenhuma linha precisa de

let.

const dentro de laço: o erro do const no for

for (let i = 0; ...) é a forma correta, porque let cria uma cópia da variável

por volta. for (const i = 0; ...) não funciona: o i++ é uma reatribuição, e

recomeçar a volta é outra.

for (const item of ['a', 'b']) {
  console.log(item);   // funciona: o item nunca e reatribuido
}

A const com função segue a mesma regra: const aponta para a caixa, e a função

não é reatribuída. É por isso que for...of e forEach aceitam const e map também: nenhum

dos três reatribui o valor que estão percorrendo.

O for...of do exemplo imprime duas linhas, item do for...of: estudar e

item do for...of: treinar, e o const em cada volta não deu problema: cada

passada cria a própria caixa para o item, e a caixa é descartada no fim. O que

não funciona é o const no for clássico, porque ali o i é reatribuído a cada

volta.

E o exemplo mostra o let na posição onde ele é o caminho certo, dentro de uma

const com função: o contador é const, e o n dentro dele é let. As

duas chamadas devolvem contador: 1 | nova chamada: 1, e esse 1 duas vezes é o

ponto: cada chamada da seta cria um n novo. Se o n fosse de fora, a segunda

chamada devolveria 2.

Object.freeze: o congelamento que o modo frouxo não cumpre

Object.freeze é o método que impede a mudança do conteúdo:

const config = Object.freeze({ host: 'localhost' });
config.host = 'outro';   // TypeError em modo estrito

O detalhe que o aluno costuma tropeçar: um arquivo .js comum roda em **modo

frouxo (sloppy mode), e nesse modo o freeze não** interrompe a execução — a

atribuição simplesmente não acontece e o valor continua o de antes. Nenhum

TypeError aparece. O congelamento só vira erro em módulo estrito, em arquivo

.mjs ou no Babel do projeto. Por isso freeze sozinho não é garantia de

imutabilidade num arquivo .js do dia a dia — a disciplina de não reatribuir o

nome continua sendo a garantia real.

O exemplo da página confirma essa Fraqueza sem depender de palpite: congelado.n

recebe 2, o console imprime freeze em modo frouxo nao interrompe: 1, e

continua 1. Nada de TypeError, nada de aviso — a escrita foi recusada em

silêncio. A linha seguinte mostra o caminho que sobra: `const copia =

{ ...congelado, n: 2 }` cria um objeto novo com o valor certo, e o console

imprime objeto novo com o valor certo: { n: 2 }.

A última linha do exemplo é a distinção que fecha a aula:

mesma referencia: false | mesmo conteudo: false. Os dois objetos são

diferentes, porque o freeze recusou a escrita no original.

Tabela: quando usar cada um

SituaçãoEscolha
O valor nunca muda depois de criadoconst
O valor muda: contador, acumulador, campo de formuláriolet
Objeto ou array que pode receber itensconst
Parâmetro de função que a função não alteraconst
Código antigo já escritovar, sem mexer

A resposta de quando usar const, e de preferir const por omissão, é curta: sempre que o valor não

muda. A preferência por const não é estilo: é proteção. const transforma o erro

de reatribuição acidental em erro visível, e um erro que o compilador aponta

custa menos que um bug que só aparece no aparelho.

Vale o contraponto, porque é onde a tabela engana. "Valor que nunca muda" é o

critério do nome; "objeto que pode receber itens" é o critério do conteúdo. Uma

lista de tarefas é const mesmo crescendo a cada item novo, e o que justifica

let é outro: o índice, o contador, o texto do campo que o usuário digita. Pelo

mesmo motivo, parâmetro de função que a função não altera entra como const

quando existe a opção — a proteção fica no lugar onde o erro seria silencioso.

Exemplo

// const nao impede mudar o conteudo, so impede reatribuir o nome.
const tarefa = { texto: 'estudar', feita: false };
tarefa.feita = true;                  // mudou o objeto: permitido
console.log('objeto:', tarefa);

try {
  tarefa = { texto: 'outra' };        // mudou o nome: TypeError
} catch (erro) {
  console.error(erro.constructor.name + ': ' + erro.message);
  console.log('erro:', erro.constructor.name, '-', erro.message);
}

// const com array: mesmo caso do objeto
const tarefas = ['estudar'];
tarefas.push('treinar');              // mudou o conteudo: permitido
console.log('array:', tarefas);

try {
  tarefas = [];
} catch (erro) {
  console.log('erro ao reatribuir o array:', erro.constructor.name, '-', erro.message);
}

// const em laco com for...of, que nunca reatribui o valor
for (const item of ['estudar', 'treinar']) {
  console.log('item do for...of:', item);
}

// const em funcao seta com estado proprio
const contador = () => {
  let n = 0;                          // aqui sim, let: o valor muda
  n = n + 1;
  return n;
};
console.log('contador:', contador(), '| nova chamada:', contador());

// Object.freeze em arquivo .js roda em modo frouxo: nao trava, e nao avisa
const congelado = Object.freeze({ n: 1 });
congelado.n = 2;
console.log('freeze em modo frouxo nao interrompe:', congelado.n);

// o unico jeito de escrita e criar um objeto novo
const copia = { ...congelado, n: 2 };
console.log('objeto novo com o valor certo:', copia);

// a diferenca entre comparar conteudo e comparar nome
console.log('mesma referencia:', congelado === copia, '| mesmo conteudo:',
  JSON.stringify(congelado) === JSON.stringify(copia));

Saída real

objeto: { texto: 'estudar', feita: true }
erro: TypeError - Assignment to constant variable.
array: [ 'estudar', 'treinar' ]
erro ao reatribuir o array: TypeError - Assignment to constant variable.
item do for...of: estudar
item do for...of: treinar
contador: 1 | nova chamada: 1
freeze em modo frouxo nao interrompe: 1
objeto novo com o valor certo: { n: 2 }
mesma referencia: false | mesmo conteudo: false