JavaScript é a linguagem que faz a página reagir: clicar, digitar, filtrar, guardar e buscar dados. É o quarto artigo da trilha "Quero virar dev em casa", e ele termina com uma lista de tarefas que funciona de verdade e guarda os dados no navegador. Pré-requisitos: lógica de programação e uma página HTML e CSS, como a do artigo 3.

Para quem é este artigo

Para quem já fez a primeira página e quer acrescentar interação. Tempo de estudo sugerido: de 10 a 15 horas ao longo de duas a três semanas, entre leitura, vídeos e prática (estimativa da Redação).

O que você vai aprender

  1. Variáveis, tipos, arrays e objetos em JavaScript.
  2. O DOM: como o JavaScript enxerga e muda a página.
  3. Eventos: reagir a cliques e formulários.
  4. fetch: carregar dados de um arquivo ou de uma API.
  5. localStorage: guardar dados no navegador.
  6. Um mini-projeto: lista de tarefas.

O que é JavaScript e por que é uma boa primeira linguagem

JavaScript é a linguagem que roda dentro do navegador. HTML dá a estrutura, CSS dá a aparência e JavaScript dá o comportamento. Por rodar em qualquer navegador, não exige instalar nada além do que você já tem, e o resultado aparece na hora. Esta é a razão pela qual a Redação recomenda começar pela web, como no primeiro artigo da trilha.

Os fundamentos que você já viu

No artigo de lógica, você usou variáveis, condições, repetições e funções, em Python e em JavaScript. A ideia é a mesma aqui. O MDN recomenda declarar variáveis com const quando puder e let quando precisar.

Tipos e comparações

console.log(typeof 42, typeof "oi", typeof true, typeof undefined);
// number string boolean undefined
console.log(3 === "3", 3 == "3");
// false true

Use sempre === (igualdade estrita), que não converte tipos. O == converte e leva a resultados surpreendentes, como 3 == "3" ser verdadeiro. Esta é uma recomendação da Redação.

Arrays: listas de valores

const tarefas = ["Estudar HTML", "Estudar CSS"];
tarefas.push("Publicar no GitHub Pages");
console.log(tarefas.length, tarefas[0]);
// 3 Estudar HTML

for (const tarefa of tarefas) {
  console.log("-", tarefa);
}

A contagem começa em zero: tarefas[0] é o primeiro item.

Objetos: dados com nome

const pessoa = { nome: "Ana", idade: 28, estuda: true };
console.log(pessoa.nome, pessoa["idade"]);
// Ana 28
pessoa.cidade = "Recife";

Arrays e objetos juntos descrevem quase qualquer dado: uma lista de tarefas é um array de objetos, cada um com texto e estado. E o método filter cria um novo array só com o que passa em um teste:

const pendentes = tarefas.filter((t) => !t.concluida);

O DOM: como o JavaScript enxerga a página

Segundo o MDN, o DOM é uma representação em árvore da estrutura do HTML, criada pelo navegador, que permite acessá-la por linguagens de programação. Cada item da árvore é um nó. O MDN recomenda selecionar elementos com querySelector, que aceita seletores de CSS:

const botao = document.querySelector("#tema");

Com a referência na mão, você pode mudar o texto (textContent), criar elementos (createElement), colocá-los na página (appendChild) e retirá-los (remove):

const item = document.createElement("li");
item.textContent = "Estudar JavaScript";
lista.appendChild(item);

Eventos: reagir ao que o usuário faz

Segundo o MDN, eventos são coisas que acontecem no sistema e sobre as quais ele avisa para o seu código reagir. Para reagir, você anexa um ouvinte de evento com addEventListener, o mecanismo recomendado pelo MDN. Ele recebe o tipo do evento (como "click") e a função a executar:

botao.addEventListener("click", () => {
  document.body.classList.toggle("escuro");
});

Em formulários, o navegador por padrão envia e recarrega a página. O MDN mostra como preventDefault() impede esse comportamento padrão, e é o que faremos na lista de tarefas.

Exemplo 1: alternar o tema e lembrar a escolha

Acrescente um botão ao cabeçalho do currículo do artigo 3 e um arquivo script.js. No HTML, dentro do <header>, depois do <nav>:

<button id="tema" type="button">Alternar tema</button>

E, antes do </body>:

<script src="script.js"></script>

No CSS, acrescente o visual escuro:

body.escuro { background: #0b1520; color: #e4ebf2; }
body.escuro h2 { color: #9fb3c8; }
body.escuro .cartoes li { background: #16283a; }
body.escuro main a { color: #9fb3c8; }

E no script.js:

const botaoTema = document.querySelector("#tema");

if (localStorage.getItem("tema") === "escuro") {
  document.body.classList.add("escuro");
}

botaoTema.addEventListener("click", () => {
  const escuro = document.body.classList.toggle("escuro");
  localStorage.setItem("tema", escuro ? "escuro" : "claro");
});

O MDN explica que o localStorage guarda pares de chave e valor, sempre como texto, e que os dados persistem mesmo quando o navegador é fechado e reaberto. A página recomenda usar setItem e getItem. Resultado testado: o botão alterna o tema, e a escolha continua depois de recarregar a página. O teste também achou um problema de contraste: os links de contato ficavam azul-escuros sobre fundo escuro, e a regra body.escuro main a acima o corrige.

Cuidado (da Redação): não guarde senhas nem dados sensíveis no localStorage; qualquer script da página consegue lê-lo.

fetch: carregar dados de um arquivo ou de uma API

Segundo o MDN, fetch() é uma API assíncrona que devolve uma Promise. Você encadeia then() para tratar a resposta, chama response.json() para obter os dados como objeto e confere response.ok, para detectar erros como o 404. Um catch() no fim trata falhas.

Crie um arquivo projetos.json:

[
  { "nome": "Currículo online", "descricao": "minha primeira página, publicada no GitHub Pages" },
  { "nome": "Lista de tarefas", "descricao": "adicionar, concluir, remover e filtrar tarefas" }
]

Acrescente <ul id="lista-projetos"></ul> na seção de projetos do HTML e, no script.js:

const listaProjetos = document.querySelector("#lista-projetos");

fetch("projetos.json")
  .then((resposta) => {
    if (!resposta.ok) {
      throw new Error(`Erro HTTP: ${resposta.status}`);
    }
    return resposta.json();
  })
  .then((projetos) => {
    for (const projeto of projetos) {
      const item = document.createElement("li");
      item.textContent = `${projeto.nome}: ${projeto.descricao}`;
      listaProjetos.appendChild(item);
    }
  })
  .catch((erro) => {
    listaProjetos.textContent = "Não foi possível carregar os projetos.";
    console.error(`Problema no fetch: ${erro.message}`);
  });

Resultado testado: os dois projetos aparecem na lista. Atenção: o fetch de um arquivo local costuma falhar se você abrir o HTML com duplo clique (endereço file://). Teste por um servidor, e o GitHub Pages já é um (a Redação rodou todos os testes por um servidor local e não testou o caso do duplo clique). Em um editor de código, procure uma extensão de servidor local, ou rode python3 -m http.server na pasta. A página do MDN não cobre async/await; ele é uma forma equivalente de escrever, e você o encontra nos vídeos.

Mini-projeto: lista de tarefas

O usuário pode adicionar, concluir, remover e filtrar tarefas, e os dados ficam salvos no navegador. Crie tarefas.html, tarefas.css e tarefas.js.

tarefas.html

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width" />
  <title>Lista de tarefas</title>
  <link rel="stylesheet" href="tarefas.css" />
</head>
<body>
  <main>
    <h1>Lista de tarefas</h1>

    <form id="formulario">
      <label for="texto">Nova tarefa</label>
      <input id="texto" type="text" maxlength="80" autocomplete="off" />
      <button type="submit">Adicionar</button>
    </form>

    <div class="filtros">
      <button type="button" data-filtro="todas">Todas</button>
      <button type="button" data-filtro="pendentes">Pendentes</button>
      <button type="button" data-filtro="concluidas">Concluídas</button>
    </div>

    <ul id="lista"></ul>
    <p id="contador"></p>
  </main>
  <script src="tarefas.js"></script>
</body>
</html>

tarefas.css

body { margin: 0; font-family: system-ui, sans-serif; background: #f5f7fa; color: #1f2933; }
main { max-width: 32rem; margin: 0 auto; padding: 1.5rem; }
form { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
form label { width: 100%; font-weight: 600; }
form input { flex: 1; padding: 0.5rem; }
button { padding: 0.5rem 0.8rem; cursor: pointer; }
.filtros { display: flex; gap: 0.5rem; margin: 1rem 0; }
.filtros button[aria-pressed="true"] { background: #102a43; color: white; }
ul { padding: 0; list-style: none; }
li { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0; border-bottom: 1px solid #d9e2ec; }
li span { flex: 1; }
li.concluida span { text-decoration: line-through; color: #7b8794; }

tarefas.js

const formulario = document.querySelector("#formulario");
const campoTexto = document.querySelector("#texto");
const lista = document.querySelector("#lista");
const contador = document.querySelector("#contador");
const botoesFiltro = document.querySelectorAll("[data-filtro]");

let tarefas = JSON.parse(localStorage.getItem("tarefas") || "[]");
let filtro = "todas";

function salvar() {
  localStorage.setItem("tarefas", JSON.stringify(tarefas));
}

function desenhar() {
  lista.textContent = "";

  const visiveis = tarefas.filter((t) => {
    if (filtro === "pendentes") return !t.concluida;
    if (filtro === "concluidas") return t.concluida;
    return true;
  });

  for (const tarefa of visiveis) {
    const item = document.createElement("li");
    if (tarefa.concluida) item.classList.add("concluida");

    const caixa = document.createElement("input");
    caixa.type = "checkbox";
    caixa.checked = tarefa.concluida;
    caixa.setAttribute("aria-label", `Concluir: ${tarefa.texto}`);
    caixa.addEventListener("change", () => {
      tarefa.concluida = caixa.checked;
      salvar();
      desenhar();
    });

    const texto = document.createElement("span");
    texto.textContent = tarefa.texto;

    const remover = document.createElement("button");
    remover.type = "button";
    remover.textContent = "Remover";
    remover.addEventListener("click", () => {
      tarefas = tarefas.filter((t) => t.id !== tarefa.id);
      salvar();
      desenhar();
    });

    item.append(caixa, texto, remover);
    lista.appendChild(item);
  }

  const pendentes = tarefas.filter((t) => !t.concluida).length;
  contador.textContent = `${pendentes} pendente(s) de ${tarefas.length}`;

  for (const botao of botoesFiltro) {
    botao.setAttribute("aria-pressed", String(botao.dataset.filtro === filtro));
  }
}

formulario.addEventListener("submit", (evento) => {
  evento.preventDefault();
  const texto = campoTexto.value.trim();
  if (texto === "") return;
  tarefas.push({ id: crypto.randomUUID(), texto, concluida: false });
  campoTexto.value = "";
  salvar();
  desenhar();
});

for (const botao of botoesFiltro) {
  botao.addEventListener("click", () => {
    filtro = botao.dataset.filtro;
    desenhar();
  });
}

desenhar();

Como a Redação testou, e o bug que o teste achou

A Redação rodou a lista no Chrome, servida por um servidor local, e verificou: tarefa adicionada, texto vazio recusado, tarefa concluída (com o texto riscado), filtros de pendentes e concluídas, remoção e contador. Depois recarregou a página, e as tarefas continuaram lá.

O teste achou um defeito na primeira versão. O identificador de cada tarefa vinha de Date.now(), a hora em milissegundos. Quando três tarefas eram criadas no mesmo milissegundo, todas ficavam com o mesmo identificador, e remover uma removia todas, porque o código remove "todas as tarefas com esse identificador". Um humano raramente clica tão rápido, mas é um defeito real, e ele se mostrou assim que o teste adicionou tarefas em sequência. A versão acima usa crypto.randomUUID(), que gera um identificador único, e foi testada de novo: remover uma tarefa remove só ela.

A lição vale para toda a sua carreira: teste o caminho feliz e também o caso esquisito. Um bug que "nunca acontece" costuma acontecer no pior momento.

Vídeos para estudar

Os quatro vídeos foram verificados quanto ao título, ao canal, à duração, à data e à possibilidade de incorporação. A Redação não os assistiu na íntegra nem verificou o que dizem.

Em português, com cerca de 55 minutos, publicado em 2 de outubro de 2025. Trata do DOM, de arrays e de objetos, os temas centrais deste artigo. A descrição divulga uma newsletter e um certificado pago do curso. Nível: iniciante.
Vídeo: Fernanda Kipper
Em português, com cerca de 29 minutos, publicado em 27 de maio de 2019. Aula do Curso de JavaScript para Iniciantes sobre ligar uma função a um evento em um formulário. Tem mais de sete anos; os conceitos seguem válidos, mas confira o MDN para a sintaxe atual. Nível: iniciante.
Vídeo: Curso em Vídeo
Em português, com cerca de 13 minutos, publicado em 16 de junho de 2025. Introdução curta ao fetch. A descrição divulga um curso pago de full stack. Nível: iniciante.
Vídeo: Hashtag Dev
Em português, com cerca de 8 horas e 32 minutos, publicado em 28 de agosto de 2024. Curso longo, para quem quer aprofundar, com manipulação do DOM. Use por partes. Nível: iniciante.
Vídeo: freeCodeCamp em Português

Exercícios práticos

  1. Contador de cliques: um botão que mostra quantas vezes foi clicado.
  2. Saudação: um campo de nome e um botão que escreve "Olá, [nome]" na página.
  3. Tema com o tema do sistema: descubra como ler a preferência de tema do computador e use-a como padrão.
  4. Editar tarefa: na lista, permita editar o texto de uma tarefa.
  5. Prioridade: acrescente uma prioridade (alta, média, baixa) a cada tarefa e ordene a lista.
  6. Erro no fetch: troque o nome do arquivo no fetch por um que não existe e veja a mensagem de erro aparecer.

Erros comuns de iniciantes

  • Selecionar o elemento antes de ele existir. Coloque o <script> no fim do <body>, como nos exemplos.
  • Usar == em vez de ===.
  • Esquecer o preventDefault() no formulário, e a página recarrega ao enviar.
  • Guardar objeto direto no localStorage. Ele só guarda texto: use JSON.stringify para gravar e JSON.parse para ler.
  • Abrir o HTML com duplo clique e usar fetch. Use um servidor local.
  • Identificadores repetidos. O bug desta matéria: use um gerador de identificador único.
  • Copiar sem entender. Escreva à mão e quebre de propósito.
  • Não olhar o console. Aperte F12 e leia as mensagens de erro.

Checklist de conclusão

  • Sei criar arrays e objetos e percorrer um array.
  • Selecionei elementos com querySelector e mudei o conteúdo da página.
  • Usei addEventListener para reagir a cliques e a formulários.
  • Carreguei dados com fetch e tratei o erro.
  • Guardei e li dados no localStorage com JSON.
  • Terminei a lista de tarefas e testei os casos esquisitos.
  • Sei abrir o console do navegador e ler um erro.

Próximo passo da trilha

Agora que você tem projetos no computador, o próximo artigo ensina Git e GitHub: guardar a história do seu código, publicar a lista de tarefas e montar um portfólio.

Perguntas frequentes

O que é JavaScript?

É a linguagem que roda no navegador e dá comportamento à página: reage a cliques, digitação e envio de formulários, muda o conteúdo e busca dados.

JavaScript é uma boa primeira linguagem?

É uma boa porta de entrada pela web, porque roda no navegador que você já tem e mostra o resultado na hora. A Redação também recomenda aprender lógica antes, como no artigo anterior. É uma recomendação, e não uma regra.

O que é o DOM?

Segundo o MDN, é uma representação em árvore da estrutura do HTML, criada pelo navegador, que permite que linguagens de programação a acessem e a modifiquem.

O que é um evento em JavaScript?

Segundo o MDN, são coisas que acontecem no sistema e sobre as quais ele avisa, para o seu código reagir, como um clique. Você reage a elas com addEventListener.

O que é fetch?

Segundo o MDN, é uma API assíncrona que devolve uma Promise e serve para buscar dados em um endereço, como um arquivo JSON ou uma API.

O que o localStorage guarda e por quanto tempo?

Segundo o MDN, guarda pares de chave e valor como texto, e os dados persistem mesmo com o navegador fechado e reaberto. Para objetos e arrays, use JSON.stringify.

Posso guardar senhas no localStorage?

Não. Qualquer script da página consegue ler o localStorage. Esta é uma recomendação da Redação.

Por que o fetch não funciona quando abro o HTML com duplo clique?

Em geral, porque o endereço começa com file:// e o navegador bloqueia a leitura de arquivos por fetch. Use um servidor local ou publique no GitHub Pages. Esta é uma observação da Redação, e o caso do endereço file:// não foi testado por ela.

Fontes