Dia 2 — Variáveis, let e const
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
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ção | Escolha |
|---|---|
| O valor nunca muda depois de criado | const |
| O valor muda: contador, acumulador, campo de formulário | let |
| Objeto ou array que pode receber itens | const |
| Parâmetro de função que a função não altera | const |
| Código antigo já escrito | var, 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