Carregando
Todos os artigos
Programação 17 min de leitura

JavaScript: Pare de Usar var. Um Guia Visual de const e let

Por que o JavaScript moderno usa const e let em vez de var: reatribuição, escopo de bloco vs. de função, o erro dos callbacks no laço, hoisting e a zona morta temporal, redeclaração, execuções reais no Node.js, um teste de velocidade e exercícios com respostas.

nbkhope
Nesta página

Nos seus primeiros vinte anos, o JavaScript tinha um único jeito de declarar uma variável: var. Aí o ES6 (também chamado de ES2015) trouxe mais dois, const e let, e hoje a maioria dos guias de estilo resume o conselho em quatro palavras: pare de usar var.

Não é só moda. O var tem esquisitices que causam erros de verdade: variáveis que vazam para fora dos blocos, laços em que todos os callbacks enxergam o mesmo número e variáveis que dá para usar antes de declarar. Neste guia você vai ver cada esquisitice rodando de verdade, aprender a regra simples para escolher entre const e let e praticar como achar e consertar erros de var. Exercícios com respostas fecham o artigo.

Todas as saídas abaixo vêm de execuções reais com o Node.js 26. Elas são iguais em qualquer navegador moderno. As mensagens de erro do JavaScript são em inglês; vamos traduzindo as importantes.

A regra numa imagem

Qual palavra-chave eu uso? Comece com const. Essa variável vai receber um valor novo com = ? Se não, fique com const: a maioria das variáveis, inclusive arrays e objetos que você muda com push ou mudando propriedades. Se sim, use let: contadores, totais, índices de laço, valores que mudam depois. E o var? Nunca em código novo: o escopo vaza para fora dos blocos e os laços dividem uma variável. Comece com const; troque para let só quando precisar reatribuir
1// O jeito antigo
2var nome = "Angela";
3
4// O jeito novo
5const nome = "Angela";
  1. Comece com const. A maioria das variáveis recebe um valor uma vez e nunca é reatribuída.
  2. Troque para let só quando precisar reatribuir a variável com = mais tarde.
  3. Não use var em código novo.

Vamos ver o porquê, um comportamento de cada vez.

const: atribuída uma vez, para sempre

Uma variável const recebe o seu valor na declaração e nunca pode ser reatribuída:

1const saudacao = "Olá";
2console.log(saudacao);
3
4try {
5  saudacao = "Tchau";
6} catch (erro) {
7  console.log(`${erro.name}: ${erro.message}`);
8}
Olá
TypeError: Assignment to constant variable.

("Atribuição a uma variável constante.") Pegamos o erro com try/catch para o programa poder imprimi-lo e continuar. Como nunca vai poder receber um valor depois, uma const precisa de um valor na hora. Este programa nem começa:

const resposta;
SyntaxError: Missing initializer in const declaration

("Falta o valor inicial na declaração const.") Essa é a graça do const: quando você lê um código e vê const total = …, sabe que total guarda aquele valor em tudo o que vem abaixo. Não precisa caçar uma linha que o muda.

O const tranca a etiqueta, não a caixa

Esta é a parte que surpreende. Você ainda pode mudar o que está dentro de um array ou objeto const:

1const pets = [];
2pets.push("gato");
3pets.push("cachorro");
4console.log(pets);
5
6const configuracoes = { tema: "claro" };
7configuracoes.tema = "escuro";
8configuracoes.tamanhoDaFonte = 16;
9console.log(configuracoes);
10
11try {
12  pets = ["hamster"];
13} catch (erro) {
14  console.log(`${erro.name}: ${erro.message}`);
15}
[ 'gato', 'cachorro' ]
{ tema: 'escuro', tamanhoDaFonte: 16 }
TypeError: Assignment to constant variable.
O const tranca a etiqueta, não a caixa. A variável pets é uma etiqueta presa para sempre a um array. pets.push("gato") e pets.push("cachorro") podem: mudam o que está dentro do mesmo array, que agora guarda "gato" e "cachorro". pets = ["hamster"] não pode: tenta mudar a etiqueta para outro array, dando TypeError: Assignment to constant variable. const quer dizer "este nome sempre aponta para este valor", não "este valor nunca muda". Para trancar o conteúdo também, use Object.freeze, que é raso

Uma variável é uma etiqueta colada num valor. O const cola a etiqueta num array para sempre, mas o array em si ainda pode mudar. Só o pets = …, que tenta mudar a etiqueta de lugar, é proibido.

Se você quiser trancar o conteúdo também, o Object.freeze faz isso, mas só um nível para dentro:

1const lugar = Object.freeze({ cidade: "Lisboa", etiquetas: ["mar"] });
2lugar.cidade = "Porto";
3lugar.etiquetas.push("sol");
4console.log(lugar);
{ cidade: 'Lisboa', etiquetas: [ 'mar', 'sol' ] }

A cidade não mudou (fora do modo estrito a atribuição é ignorada em silêncio; dentro dele, gera um erro), mas o array lá dentro ainda pôde crescer.

let: quando o valor precisa mudar

Use let quando a variável recebe valores novos de verdade com o tempo: contadores, totais, índices de laço ou um valor decidido por um if:

1let total = 0;
2for (const preco of [3.5, 4.25, 2]) {
3  total += preco;
4}
5console.log(total);
6
7let mensagem = "Bom dia";
8const hora = 15;
9if (hora >= 12) {
10  mensagem = "Boa tarde";
11}
12console.log(mensagem);
9.75
Boa tarde

Repare que o laço usa const preco: cada volta de um laço for…of ganha um preco novinho, então ele nunca é reatribuído. Um laço de contagem clássico é diferente, porque o indice++ reatribui a variável:

1const letras = ["a", "b", "c"];
2for (let indice = 0; indice < letras.length; indice++) {
3  console.log(indice, letras[indice]);
4}
0 a
1 b
2 c

Escopo: onde uma variável existe

Escopo é a região do código onde uma variável pode ser usada. É aqui que var e let/const mais diferem:

  • let e const têm escopo de bloco: existem só dentro do par de chaves { } mais próximo, como o corpo de um if ou de um laço.
  • var tem escopo de função: ignora os blocos e pertence à função inteira em volta dele.
Escopo de bloco vs. escopo de função. Uma função contém um bloco if. let contida = "let" e const rotulo = "x", declaradas dentro das chaves do if, existem só dentro desse bloco: usá-las depois da chave de fechamento dá ReferenceError. var vazada = "var", declarada dentro do mesmo bloco if, pertence à função inteira, então continua lá depois que o bloco termina. let e const vivem dentro das { } mais próximas. var ignora as { } e vive na função inteira (ou no script inteiro, se não estiver numa função)
1function confereEscopos() {
2  if (true) {
3    var vazada = "var";
4    let contida = "let";
5  }
6  console.log(vazada);
7  try {
8    console.log(contida);
9  } catch (erro) {
10    console.log(`${erro.name}: ${erro.message}`);
11  }
12}
13
14confereEscopos();
var
ReferenceError: contida is not defined

("contida não está definida.") O mesmo acontece com as variáveis de laço. Um índice var sobrevive ao laço:

1for (var indiceAntigo = 0; indiceAntigo < 3; indiceAntigo++) {
2  console.log(indiceAntigo);
3}
4console.log("depois do laço com var:", indiceAntigo);
5
6for (let indiceNovo = 0; indiceNovo < 3; indiceNovo++) {
7  console.log(indiceNovo);
8}
9try {
10  console.log("depois do laço com let:", indiceNovo);
11} catch (erro) {
12  console.log(`${erro.name}: ${erro.message}`);
13}
10
21
32
4depois do laço com var: 3
50
61
72
8ReferenceError: indiceNovo is not defined

Uma variável vazada pode parecer inofensiva, mas quer dizer que um contador de laço ou um valor temporário pode se chocar com outra variável de mesmo nome em outro ponto da função. O escopo de bloco deixa cada variável no seu lugar.

O erro clássico do laço

Este é o erro que fez muita gente trocar de vez. Cada volta do laço agenda uma função para rodar um instante depois:

1for (var indice = 0; indice < 3; indice++) {
2  setTimeout(() => console.log("var:", indice), 0);
3}
4
5for (let indice = 0; indice < 3; indice++) {
6  setTimeout(() => console.log("let:", indice), 0);
7}
1var: 3
2var: 3
3var: 3
4let: 0
5let: 1
6let: 2
Por que o laço com var imprime 3, 3, 3. Com var existe uma única variável indice para o laço inteiro. As três funções lembram essa mesma variável e, quando rodam, o laço já terminou e indice vale 3, então imprimem 3, 3, 3. Com let, cada volta ganha a sua própria cópia de indice, guardando 0, 1 e 2, e cada função lembra a sua cópia, então imprimem 0, 1, 2. Os callbacks rodam depois que o laço termina: setTimeout, cliques e promises funcionam assim

As funções rodam depois que o laço terminou. Com var existe um único indice para o laço inteiro e, nessa hora, ele vale 3. Com let, o JavaScript cria um indice novo para cada volta, então cada função lembra o seu próprio valor. O mesmo erro aparece com tratadores de clique, temporizadores e promises criados num laço, e o let conserta todos sem nenhum código a mais.

Hoisting e a zona morta temporal

O JavaScript prepara as variáveis de uma função antes de rodar a primeira linha dela. Isso se chama hoisting ("içamento"). O que uma variável guarda antes da linha da sua declaração é onde as palavras-chave diferem:

1function antesDeDeclarar() {
2  console.log("var antes da linha:", cedo);
3  var cedo = "pronto";
4  console.log("var depois da linha:", cedo);
5
6  try {
7    console.log(tarde);
8  } catch (erro) {
9    console.log(`${erro.name}: ${erro.message}`);
10  }
11  let tarde = "pronto";
12}
13
14antesDeDeclarar();
var antes da linha: undefined
var depois da linha: pronto
ReferenceError: Cannot access 'tarde' before initialization
Hoisting e a zona morta temporal, linha por linha numa função. Com var cedo: do começo da função até a linha var cedo = "pronto", cedo existe e guarda undefined em silêncio; depois dessa linha guarda "pronto". Com let tarde: do começo da função até a linha let tarde = "pronto" fica a zona morta temporal, onde qualquer uso gera ReferenceError: Cannot access 'tarde' before initialization; depois da linha guarda "pronto". O var esconde o engano entregando undefined; let e const avisam na hora

Um var existe desde o topo da função e guarda undefined em silêncio até a sua linha rodar, então um engano dá um valor errado em vez de um erro. Um let ou const também existe desde o topo do seu bloco, mas mexer nele antes da sua linha gera um erro ("não é possível acessar 'tarde' antes da inicialização"). Esse trecho se chama zona morta temporal (temporal dead zone). Erros barulhentos são bons: apontam para o engano em vez de deixar o undefined viajar pelo programa.

Declarando o mesmo nome duas vezes

O var deixa você declarar uma variável de novo sem um pio, o que pode sobrescrever em silêncio uma que já está em uso:

var pontos = 10;
var pontos = 0;
console.log(pontos);
0

Com let ou const, o programa se recusa a rodar:

let pontos = 10;
let pontos = 0;
SyntaxError: Identifier 'pontos' has already been declared

("O identificador 'pontos' já foi declarado.") E mais uma esquisitice do var: num script comum de navegador, um var declarado no nível de cima vira uma propriedade do objeto global window, onde qualquer outro script da página pode lê-lo ou sobrescrevê-lo. Um let ou const no nível de cima não vira.

Algum deles é mais rápido?

Não. Medimos o mesmo laço de soma escrito dos três jeitos, com 50 milhões de voltas:

var, let e const têm a mesma velocidade. Tempo para rodar o mesmo laço de soma 50 milhões de vezes, medido para este artigo com o Node.js 26.3 no Windows: mediana de 7 execuções, depois a mediana de 3 execuções do programa. var: 64,8 milissegundos. let: 62,7 milissegundos. const: 63,9 milissegundos. Cerca de 1,3 nanossegundo por volta para os três. As diferenças são ruído da medição. Escolha pela correção e pela clareza, não pela velocidade

Os três deram cerca de 1,3 nanossegundo por volta, e as pequenas diferenças mudavam de uma execução para outra. Os motores de JavaScript compilam os três para o mesmo código rápido, então escolha só pela clareza e pela correção.

Lado a lado

varletconst
Pode ser reatribuídaSimSimNão
Precisa de um valor na horaNãoNãoSim
EscopoA função inteiraO bloco { } mais próximoO bloco { } mais próximo
Cópia nova a cada volta do laçoNão (uma variável compartilhada)SimSim (no for…of / for…in)
Usada antes da sua linhaundefinedReferenceErrorReferenceError
Declarada duas vezesPermitidoSyntaxErrorSyntaxError
No nível de cima vira propriedade do windowSimNãoNão
Usar em código novoNãoPara reatribuirPor padrão
Uma comparação de var, let e const em seis perguntas. Reatribuir depois: var sim, let sim, const não. Escopo: var a função inteira, let e const as { } mais próximas. Callbacks num laço: com var dividem uma variável, com let e const ganham uma nova por volta. Usada antes da linha: var dá undefined, let e const geram ReferenceError. Declarada duas vezes: var permite em silêncio, let e const dão SyntaxError. Veredito: var evite em código novo, let para reatribuir, const por padrão

Modernizando código antigo

Converter código com var é quase mecânico. Pegue esta função:

1function resumir(precos) {
2  var total = 0;
3  var quantidade = precos.length;
4  for (var indice = 0; indice < quantidade; indice++) {
5    total += precos[indice];
6  }
7  var media = total / quantidade;
8  return "Total " + total + ", média " + media.toFixed(2);
9}
10
11console.log(resumir([3.5, 4.25, 2]));
Total 9.75, média 3.25

Passe por cada var: ela é reatribuída em algum momento depois da declaração? total é (+=) e indice também (++): viram let. quantidade e media nunca são: viram const.

1function resumir(precos) {
2  let total = 0;
3  const quantidade = precos.length;
4  for (let indice = 0; indice < quantidade; indice++) {
5    total += precos[indice];
6  }
7  const media = total / quantidade;
8  return `Total ${total}, média ${media.toFixed(2)}`;
9}
10
11console.log(resumir([3.5, 4.25, 2]));
Total 9.75, média 3.25

A mesma saída, e agora cada declaração diz a quem lê se o valor muda ou não. (A template string com crases é outra melhoria do ES6.)

Sua vez: pratique

Tente fazer estes antes de espiar as respostas.

Exercício 1: Adivinhe a saída (aquecimento). O que as três linhas com console.log imprimem?

1const cores = ["vermelho"];
2cores.push("verde");
3console.log(cores.length);
4
5let contagem = 1;
6contagem = contagem + 1;
7console.log(contagem);
8
9var tom = "escuro";
10if (true) {
11  var tom = "claro";
12}
13console.log(tom);

Exercício 2: const ou let? Escolha uma palavra-chave para cada variável: o ano de nascimento de um usuário, uma pontuação que sobe durante um jogo, uma lista de tarefas em que você vai acrescentando itens e o resultado de Math.max(...numeros).

Exercício 3: Conserte os botões. Este código deveria informar "Clicou no botão 0", "Clicou no botão 1" e "Clicou no botão 2", mas todo tratador informa o botão 3. Conserte.

1const tratadores = [];
2for (var numeroDoBotao = 0; numeroDoBotao < 3; numeroDoBotao++) {
3  tratadores.push(() => console.log("Clicou no botão " + numeroDoBotao));
4}
5tratadores.forEach((tratador) => tratador());

Exercício 4: Ache o erro. O que acontece quando isto roda, e por quê?

1function cumprimentar() {
2  console.log(mensagem);
3  const mensagem = "Oi!";
4}
5cumprimentar();

Exercício 5: Modernize (desafio). Troque cada var por const ou let, escolhendo const sempre que der, e garanta que a saída não mude.

1var palavras = ["árvore", "céu", "abacaxi", "sol"];
2var maior = "";
3for (var indice = 0; indice < palavras.length; indice++) {
4  var palavra = palavras[indice];
5  if (palavra.length > maior.length) {
6    maior = palavra;
7  }
8}
9console.log("Maior palavra: " + maior);

Quiz rápido

  1. Dá para fazer push num array declarado com const?
  2. Onde existe uma variável declarada com let dentro de um bloco if?
  3. Por que um laço com var e setTimeout imprime o mesmo número três vezes?
  4. O que você ganha ao usar um var antes da linha da sua declaração? E um let?
  5. Com qual palavra-chave você deve começar?

Respostas

Exercício 1: O push muda o conteúdo do array, o que o const permite; contagem é reatribuída, o que o let permite; e o segundo var tom nem é uma variável nova, porque o var ignora o bloco do if e só reatribui o mesmo tom:

1const cores = ["vermelho"];
2cores.push("verde");
3console.log(cores.length);
4
5let contagem = 1;
6contagem = contagem + 1;
7console.log(contagem);
8
9var tom = "escuro";
10if (true) {
11  var tom = "claro";
12}
13console.log(tom);
2
2
claro

Com let tom nas duas vezes, a de dentro seria outra variável, vivendo só no bloco, e a última linha imprimiria escuro.

Exercício 2: Ano de nascimento: const (nunca muda). Pontuação do jogo: let (pontos += 10 a reatribui). Lista de tarefas: const (o push muda o conteúdo, não a variável). Math.max(...numeros): const (é calculado uma vez).

Exercício 3: Troque var por let, para cada volta ganhar o seu próprio numeroDoBotao:

1const tratadores = [];
2for (let numeroDoBotao = 0; numeroDoBotao < 3; numeroDoBotao++) {
3  tratadores.push(() => console.log("Clicou no botão " + numeroDoBotao));
4}
5tratadores.forEach((tratador) => tratador());
Clicou no botão 0
Clicou no botão 1
Clicou no botão 2

Exercício 4: Dá erro, porque mensagem é usada dentro da sua zona morta temporal, antes da linha do const:

1function cumprimentar() {
2  try {
3    console.log(mensagem);
4  } catch (erro) {
5    console.log(`${erro.name}: ${erro.message}`);
6  }
7  const mensagem = "Oi!";
8}
9cumprimentar();
ReferenceError: Cannot access 'mensagem' before initialization

Com var mensagem, imprimiria undefined e esconderia o engano. O conserto é declarar a variável antes de usá-la.

Exercício 5: palavras nunca é reatribuída, então é const (só a lemos). maior muda dentro do laço, então é let. indice é reatribuído pelo indice++: let. palavra é uma variável nova a cada volta e nunca é reatribuída dentro de uma: const.

1const palavras = ["árvore", "céu", "abacaxi", "sol"];
2let maior = "";
3for (let indice = 0; indice < palavras.length; indice++) {
4  const palavra = palavras[indice];
5  if (palavra.length > maior.length) {
6    maior = palavra;
7  }
8}
9console.log("Maior palavra: " + maior);
Maior palavra: abacaxi

Mais caprichado ainda: for (const palavra of palavras) elimina o índice de vez.

Quiz: (1) Sim: o const impede reatribuir a variável, não mudar o conteúdo do array. (2) Só dentro daquele bloco, entre as suas chaves, da linha da declaração em diante. (3) Existe uma única variável var para o laço inteiro, e os callbacks rodam depois que o laço termina, quando ela já vale 3. (4) Um var dá undefined; um let gera ReferenceError: Cannot access … before initialization. (5) const.

Cola rápida

SituaçãoUse
Um valor que você define uma vezconst
Um array ou objeto em que você acrescenta ou muda coisasconst
Um contador, total ou indicador que você reatribuilet
O índice de um laço clássico for (… ; … ; indice++)let
A variável de um laço for…ofconst
Um valor escolhido por um if / elselet (ou const com o operador ? :)
Um conteúdo que nunca pode mudarconst mais Object.freeze
Código novoNunca var

Resumindo

const e let não são só jeitos novos de escrever var; eles consertam as maiores armadilhas dele:

  • Comece com const, e troque para let só quando reatribuir com =.
  • O const tranca a variável, não o valor: arrays e objetos ainda podem mudar por dentro.
  • let e const têm escopo de bloco; o var vaza dos blocos para a função inteira.
  • Laços com let dão a cada volta a sua própria variável, consertando o erro de "todos os meus callbacks veem 3".
  • Usar let ou const cedo demais gera um erro (a zona morta temporal) em vez de entregar undefined.
  • Redeclarar é um erro com let e const.
  • A velocidade é a mesma: cerca de 1,3 nanossegundo por volta de laço para os três no nosso teste.

Para os detalhes, veja as páginas do MDN sobre const, let e var.

Obrigado pela leitura!

Conhece alguém que vai gostar? Mande para essa pessoa.

Mais artigos

Leia a seguir

Comentários

Seja gentil e específico. Comentários com links de contas novas aparecem depois da análise de um moderador.