O que é o DOM e como o JavaScript interage com o HTML

[66] O que é o DOM e como o JavaScript interage com o HTML

O navegador não guarda o seu HTML como texto: ele o transforma numa árvore de objetos, e é nessa árvore que o JavaScript mexe. Este é o artigo que troca o console pela página real — querySelector e classList, a diferença entre textContent e innerHTML, e por que a segunda nunca deve receber dado vindo do usuário.
Javascript

18 min de leitura

Até agora todo o nosso código rodou no console — um ambiente controlado, sem interface visual. A partir deste módulo mudamos completamente de cenário. Vamos aprender a manipular páginas web de verdade, reagir a cliques, criar e remover elementos, validar formulários e dar vida a interfaces.

Para isso, precisamos primeiro entender o DOM — o conceito central de todo desenvolvimento front-end com JavaScript.

O que é o DOM?

DOM significa Document Object Model — Modelo de Objeto do Documento. Quando o navegador carrega um arquivo HTML, ele não enxerga texto. Ele parseia (interpreta) esse texto e constrói uma representação em memória da página — uma árvore de objetos que o JavaScript pode ler e modificar.

Considere este HTML simples:

<!DOCTYPE html>
<html>
  <head>
    <title>Minha Página</title>
  </head>
  <body>
    <h1 id="titulo">Olá, mundo!</h1>
    <p class="descricao">Bem-vindo ao curso.</p>
    <ul>
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </ul>
  </body>
</html>

O navegador transforma isso em uma árvore de nós:

document
└── html
    ├── head
    │   └── title → "Minha Página"
    └── body
        ├── h1#titulo → "Olá, mundo!"
        ├── p.descricao → "Bem-vindo ao curso."
        └── ul
            ├── li → "Item 1"
            ├── li → "Item 2"
            └── li → "Item 3"

Cada elemento HTML vira um nessa árvore. E o JavaScript pode acessar, modificar, criar e remover qualquer nó dessa árvore em tempo real — sem recarregar a página.

O objeto document

O ponto de entrada para o DOM é o objeto global document. Ele representa a página inteira e é o ponto de partida para qualquer manipulação:

console.log(document);               // o documento inteiro
console.log(document.title);         // "Minha Página"
console.log(document.URL);           // URL da página atual
console.log(document.body);          // o elemento <body>
console.log(document.head);          // o elemento <head>
console.log(document.documentElement); // o elemento <html>

Selecionando elementos

Para manipular um elemento, primeiro você precisa selecioná-lo. Existem vários métodos:

getElementById

Seleciona um único elemento pelo seu id:

const titulo = document.getElementById("titulo");
console.log(titulo);         // <h1 id="titulo">Olá, mundo!</h1>
console.log(titulo.textContent); // "Olá, mundo!"

getElementsByClassName

Seleciona todos os elementos com uma determinada classe — retorna uma HTMLCollection (parecida com array, mas não é):

const descricoes = document.getElementsByClassName("descricao");
console.log(descricoes[0].textContent); // "Bem-vindo ao curso."

getElementsByTagName

Seleciona todos os elementos de uma tag:

const itens = document.getElementsByTagName("li");
console.log(itens.length); // 3

querySelector e querySelectorAll — os modernos

Estes dois métodos usam seletores CSS e são os mais usados hoje em dia:

// querySelector — retorna o PRIMEIRO elemento que corresponde
const titulo = document.querySelector("#titulo");
const descricao = document.querySelector(".descricao");
const primeiroItem = document.querySelector("li");
const inputEmail = document.querySelector("input[type='email']");

// querySelectorAll — retorna TODOS os elementos correspondentes (NodeList)
const todosItens = document.querySelectorAll("li");
const botoesAtivos = document.querySelectorAll(".btn.ativo");

console.log(todosItens.length); // 3

// NodeList pode ser percorrida com forEach
todosItens.forEach(item => {
  console.log(item.textContent);
});
// Item 1
// Item 2
// Item 3

Na prática moderna, use quase exclusivamente querySelector e querySelectorAll. Eles são flexíveis, consistentes e usam a mesma sintaxe do CSS que você já conhece.

Lendo e modificando conteúdo

Com o elemento selecionado, você pode ler e modificar seu conteúdo:

const titulo = document.querySelector("#titulo");

// Lendo conteúdo
console.log(titulo.textContent); // "Olá, mundo!" — texto puro
console.log(titulo.innerHTML);   // "Olá, mundo!" — HTML interno

// Modificando conteúdo
titulo.textContent = "Bem-vindo ao JavaScript!";

// innerHTML interpreta tags HTML
titulo.innerHTML = "Bem-vindo ao <strong>JavaScript</strong>!";

Atenção: use textContent quando estiver inserindo texto simples. Use innerHTML apenas quando precisar inserir HTML — e nunca com dados vindos do usuário, pois isso abre vulnerabilidades de segurança (XSS).

Lendo e modificando atributos

const link = document.querySelector("a");

// Lendo atributos
console.log(link.getAttribute("href"));  // "https://..."
console.log(link.id);
console.log(link.className);

// Modificando atributos
link.setAttribute("href", "https://developer.mozilla.org");
link.setAttribute("target", "_blank");

// Verificando se existe
console.log(link.hasAttribute("href")); // true

// Removendo
link.removeAttribute("target");

Manipulando estilos CSS

const titulo = document.querySelector("#titulo");

// Modificando estilos inline
titulo.style.color = "royalblue";
titulo.style.fontSize = "2.5rem";
titulo.style.fontWeight = "bold";
titulo.style.backgroundColor = "#f0f4ff";
titulo.style.padding = "1rem";

Mas modificar estilos diretamente com JavaScript é considerado má prática para a maioria dos casos. A forma profissional é manipular classes CSS:

Manipulando classes CSS — classList

O classList é uma das APIs mais usadas no desenvolvimento front-end:

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

// Adicionar classe
botao.classList.add("ativo");
botao.classList.add("destacado", "grande"); // múltiplas de uma vez

// Remover classe
botao.classList.remove("destacado");

// Alternar (adiciona se não tem, remove se tem)
botao.classList.toggle("ativo");

// Verificar se tem uma classe
console.log(botao.classList.contains("ativo")); // true ou false

// Substituir uma classe por outra
botao.classList.replace("grande", "pequeno");

Este padrão é o correto: você define os estilos no CSS e usa o JavaScript apenas para adicionar ou remover classes:

/* No CSS */
.ativo {
  background-color: royalblue;
  color: white;
}

.oculto {
  display: none;
}
// No JavaScript — apenas gerencia as classes
botao.classList.toggle("ativo");
modal.classList.add("oculto");

Percorrendo a árvore do DOM

O DOM é uma árvore e você pode navegar entre os nós:

const lista = document.querySelector("ul");

console.log(lista.parentElement);      // elemento pai — <body>
console.log(lista.children);           // filhos diretos — HTMLCollection de <li>s
console.log(lista.firstElementChild);  // primeiro filho — primeiro <li>
console.log(lista.lastElementChild);   // último filho — último <li>

const primeiroItem = lista.firstElementChild;
console.log(primeiroItem.nextElementSibling);     // segundo <li>
console.log(primeiroItem.previousElementSibling); // null (é o primeiro)

Um exemplo completo

Vamos criar uma pequena página e manipulá-la com JavaScript:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>DOM em ação</title>
  <style>
    .destaque { color: royalblue; font-weight: bold; }
    .oculto { display: none; }
    .concluido { text-decoration: line-through; color: gray; }
  </style>
</head>
<body>
  <h1 id="titulo">Lista de Tarefas</h1>
  <p id="contador">0 tarefas</p>

  <ul id="lista">
    <li>Estudar JavaScript</li>
    <li>Praticar no console</li>
    <li>Construir um projeto</li>
  </ul>

  <div id="info" class="oculto">
    Você passou o mouse em: <span id="itemAtual"></span>
  </div>

  <script>
    // Selecionar elementos
    const titulo = document.querySelector("#titulo");
    const contador = document.querySelector("#contador");
    const lista = document.querySelector("#lista");
    const info = document.querySelector("#info");
    const itemAtual = document.querySelector("#itemAtual");
    const itens = document.querySelectorAll("li");

    // Atualizar o título
    titulo.classList.add("destaque");

    // Atualizar contador
    contador.textContent = `${itens.length} tarefas`;

    // Marcar primeiro item como concluído
    lista.firstElementChild.classList.add("concluido");

    // Interagir com cada item
    itens.forEach(item => {
      // Ao passar o mouse — mostra info
      item.addEventListener("mouseenter", () => {
        itemAtual.textContent = item.textContent;
        info.classList.remove("oculto");
      });

      // Ao tirar o mouse — oculta info
      item.addEventListener("mouseleave", () => {
        info.classList.add("oculto");
      });

      // Ao clicar — alterna concluído
      item.addEventListener("click", () => {
        item.classList.toggle("concluido");
      });
    });
  </script>
</body>
</html>

Salve como .html, abra no navegador e observe o DOM funcionando em tempo real. Veja como o JavaScript e o CSS trabalham juntos — JS gerencia as classes, CSS cuida da aparência.

DevTools — sua ferramenta de trabalho

Pressione F12 no navegador, vá na aba Elements (ou Elementos). Você verá a árvore do DOM ao vivo. Clique em qualquer elemento e no console digite:

// $0 sempre referencia o elemento selecionado no DevTools
console.log($0);
$0.style.border = "2px solid red";
$0.classList.add("destaque");

O DevTools é seu laboratório. Experimente tudo por lá antes de escrever no código.

Boas práticas ao trabalhar com DOM

// ✅ 1. Sempre verifique se o elemento existe antes de manipulá-lo
const botao = document.querySelector("#botaoEnviar");
if (botao) {
  botao.classList.add("ativo");
}

// ✅ 2. Cache seus seletores — não selecione o mesmo elemento várias vezes
// ❌ Ruim — seleciona 3 vezes
document.querySelector("#titulo").textContent = "Novo título";
document.querySelector("#titulo").classList.add("destaque");
document.querySelector("#titulo").style.display = "block";

// ✅ Bom — seleciona uma vez e reutiliza
const titulo = document.querySelector("#titulo");
titulo.textContent = "Novo título";
titulo.classList.add("destaque");
titulo.style.display = "block";

// ✅ 3. Prefira classList a style para manipular aparência
// ❌ Evite
elemento.style.color = "blue";
elemento.style.fontWeight = "bold";

// ✅ Prefira
elemento.classList.add("destaque");

// ✅ 4. Use textContent para texto simples — nunca innerHTML com dados do usuário
const input = document.querySelector("#busca");
const resultado = document.querySelector("#resultado");

// ❌ Vulnerável a XSS
resultado.innerHTML = input.value;

// ✅ Seguro
resultado.textContent = input.value;

Tarefa para você

Crie um arquivo HTML com a seguinte estrutura e manipule-o com JavaScript:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>Exercício DOM</title>
</head>
<body>
  <h1 id="titulo">Meu Perfil</h1>
  <img id="avatar" src="https://via.placeholder.com/100" alt="Avatar">
  <p id="nome">Nome: ???</p>
  <p id="profissao">Profissão: ???</p>
  <ul id="habilidades"></ul>

  <script>
    // 1. Altere o título para seu nome
    // 2. Altere os parágrafos com seus dados reais
    // 3. Adicione 3 habilidades à lista dinamicamente
    //    (crie elementos <li> com JavaScript)
    // 4. Aplique uma cor de destaque ao título via classList
    // 5. Altere o src do avatar para uma imagem real
  </script>
</body>
</html>
Ver solução — as cinco tarefas do script, com o DOM certo para cada uma
// Cole no lugar do <script> do exercício. A página é a mesma do
// enunciado: #titulo, #avatar, #nome, #profissao e a <ul> #habilidades.

// ---------------------------------------------------------------
// 1 — o título
// ---------------------------------------------------------------
const titulo = document.querySelector("#titulo");
titulo.textContent = "Ana Prado";

// textContent, não innerHTML: o nome é TEXTO. Se viesse de um campo
// preenchido pelo usuário, innerHTML executaria o que houvesse ali —
// é assim que nasce um XSS.
// titulo.innerHTML = nomeDigitado;  // ❌ nunca com dado de fora

// ---------------------------------------------------------------
// 2 — os parágrafos
// ---------------------------------------------------------------
const perfil = {
  nome: "Ana Prado",
  profissao: "Desenvolvedora front-end",
  habilidades: ["JavaScript", "CSS", "Acessibilidade"],
  avatar: "https://avatars.githubusercontent.com/u/9919?s=200",
};

document.querySelector("#nome").textContent = `Nome: ${perfil.nome}`;
document.querySelector("#profissao").textContent = `Profissão: ${perfil.profissao}`;

// ---------------------------------------------------------------
// 3 — as habilidades, criadas do zero
// ---------------------------------------------------------------
const listaHabilidades = document.querySelector("#habilidades");

// O fragment junta os três <li> na memória e encosta no documento uma
// vez só. Com três itens tanto faz; com trezentos, é a diferença entre
// um reflow e trezentos.
const fragmento = document.createDocumentFragment();

for (const habilidade of perfil.habilidades) {
  const item = document.createElement("li");
  item.textContent = habilidade;
  item.classList.add("habilidade");
  fragmento.appendChild(item);
}

listaHabilidades.appendChild(fragmento);

// ---------------------------------------------------------------
// 4 — a cor de destaque, via classList
// ---------------------------------------------------------------
// classList em vez de style.color: a decisão de COMO fica bonito é do
// CSS. O JavaScript só diz em que estado o elemento está.
titulo.classList.add("destaque");

// A folha de estilo (no <head> da página):
// .destaque { color: #5c6bc0; border-bottom: 3px solid #5c6bc0;
//             display: inline-block; padding-bottom: .25rem; }
// .habilidade { display: inline-block; background: #eef1ff;
//               border-radius: 999px; padding: .2rem .8rem;
//               margin: .15rem; font-size: .9rem; }

// Os quatro métodos que importam:
titulo.classList.add("destaque");        // adiciona (não duplica)
titulo.classList.remove("inexistente");  // remove (não reclama se não tem)
titulo.classList.toggle("destaque");     // liga/desliga — aqui DESLIGA
titulo.classList.toggle("destaque");     // e liga de novo
console.log(titulo.classList.contains("destaque")); // true

// ---------------------------------------------------------------
// 5 — o avatar
// ---------------------------------------------------------------
const avatar = document.querySelector("#avatar");

avatar.src = perfil.avatar;
avatar.alt = `Foto de ${perfil.nome}`; // alt junto: imagem sem alt é imagem invisível

// Imagem externa falha — e sem tratamento a página fica com o ícone
// quebrado. O evento error dá a chance de cair num placeholder.
avatar.addEventListener("error", () => {
  avatar.src = "https://via.placeholder.com/100?text=sem+foto";
  avatar.alt = "Avatar indisponível";
});

// ---------------------------------------------------------------
// O detalhe que morde: o script tem de rodar DEPOIS do HTML existir
// ---------------------------------------------------------------
// No exercício o <script> está no fim do <body>, então tudo acima já
// existe quando ele roda. Movido para o <head>, todo querySelector
// devolveria null e a primeira atribuição estouraria:
//
//   TypeError: Cannot set properties of null (setting 'textContent')
//
// As duas saídas:
//   <script defer src="app.js"></script>   ← no <head>, roda após o parse
//   document.addEventListener("DOMContentLoaded", () => { ... });

querySelector devolve null quando não acha — não lança erro. O erro só aparece uma linha depois, ao mexer no null, e a mensagem aponta para a linha errada. Se o seletor está certo e o retorno é null, a causa quase sempre é ordem: o script rodou antes de o elemento existir.

O DOM não é o HTML: é a árvore de objetos que o navegador constrói a partir dele, e essa diferença é o que explica por que alterar a página pelo JavaScript não muda o arquivo no servidor. Para selecionar, querySelector e querySelectorAll dão conta de quase tudo — os métodos mais antigos continuam existindo, mas raramente compensam. E entre textContent e innerHTML a escolha é de segurança antes de ser de gosto: o primeiro trata o que recebe como texto, o segundo interpreta como marcação.

Fontes e Referências

Exercícios

Exercício 1

Com a lista abaixo na página, o que cada linha marcada devolve — e por que a última quebra?

<ul id="lista">
  <li class="item">A</li>
  <li class="item">B</li>
  <li class="item">C</li>
</ul>
const vivos     = document.getElementsByClassName("item");
const estaticos = document.querySelectorAll(".item");

document.querySelector("#lista").firstElementChild.remove();

console.log(vivos.length);      // A
console.log(estaticos.length);  // B
estaticos.forEach(li => console.log(li.textContent)); // C
vivos.forEach(li => console.log(li.textContent));     // D
Ver resposta

✓ Resposta: A é 2 e B é 3. A HTMLCollection de getElementsByClassName é viva: ela não guarda elementos, guarda uma consulta, e reflete o documento no instante em que você a consulta. Já a NodeList de querySelectorAll é um retrato tirado na hora da chamada e não muda mais. C imprime A, B e C — o <li> removido saiu do documento, mas continua existindo como objeto na memória, e a NodeList ainda o segura. D lança TypeError: vivos.forEach is not a function: NodeList ganhou forEach, HTMLCollection não. Para percorrê-la, Array.from(vivos) ou [...vivos] — e a conversão, de quebra, congela a coleção, o que evita o clássico laço que remove elementos e pula a metade deles porque a lista encolhe embaixo dele.

Exercício 2

O usuário digitou o texto abaixo num campo de busca. Descreva o que aparece na tela em cada uma das duas linhas.

const alvo = document.querySelector("#resultado");
const digitado = '<img src="x" onerror="roubarSessao()">';

alvo.textContent = digitado; // A
alvo.innerHTML   = digitado; // B
Ver resposta

✓ Resposta: Em A aparece na tela, literalmente, o texto <img src="x" onerror="roubarSessao()">. O textContent não interpreta nada: ele escreve caracteres, e o navegador escapa os sinais de menor e maior sozinho. Em B não aparece imagem nenhuma visível — e é justamente esse o problema. O innerHTML manda o navegador parsear a string, o que cria um elemento <img> de verdade apontando para x, que não existe; o carregamento falha, o navegador dispara o evento error e executa roubarSessao(). Isso é um XSS, e ele não precisa de <script> para acontecer — filtrar a palavra "script" da entrada não protege nada. A regra prática é a do artigo, sem exceção: dado que veio de fora entra por textContent. O innerHTML fica para HTML que você escreveu.

Exercício 3

A regra .destaque { color: royalblue; } está na folha de estilo. O que sai em cada linha?

const titulo = document.querySelector("#titulo");
titulo.classList.add("destaque");

console.log(titulo.style.color);             // A
console.log(getComputedStyle(titulo).color); // B

titulo.style.color = "red";
console.log(titulo.classList.contains("destaque")); // C
Ver resposta

✓ Resposta: A é a string vazia "". A propriedade style é uma janela para o atributo style="" da tag e nada mais — ela não sabe nada de folha de estilo, e a cor veio da classe. B é "rgb(65, 105, 225)": o getComputedStyle resolve a cascata inteira e sempre devolve o valor já computado, em rgb(), nunca o nome que você escreveu. C é true — atribuir um estilo inline não remove classe alguma; ele apenas passa na frente na cascata. E é aí que mora o incômodo de misturar os dois: uma vez que existe estilo inline, a classe só volta a mandar com !important, e o jeito limpo de desfazer é titulo.style.color = "", que apaga a declaração inline e devolve o comando ao CSS.

Exercício 4

O HTML está indentado, como todo HTML escrito por gente. Quanto dá cada uma das quatro linhas?

<ul id="lista">
  <li>Item 1</li>
  <li>Item 2</li>
</ul>
const lista = document.querySelector("#lista");

console.log(lista.children.length);   // A
console.log(lista.childNodes.length); // B
console.log(lista.firstChild);        // C
console.log(lista.firstElementChild); // D
Ver resposta

✓ Resposta: A é 2 e B é 5. O children conta só elementos; o childNodes conta todos os nós, e a quebra de linha com a indentação entre as tags é um nó de texto legítimo — há um antes do primeiro <li>, um entre os dois e um depois do segundo, somando três nós de texto mais os dois elementos. Por isso C não é o <li>: é um #text com uma quebra de linha e dois espaços dentro. D é o <li>Item 1</li> que você esperava. É exatamente para isso que existem as variantes com Element no nome — firstElementChild, lastElementChild, nextElementSibling —: elas pulam texto e comentário. O detalhe traiçoeiro é que, se o HTML vier minificado numa linha só, childNodes.length passa a valer 2 e firstChild acerta o <li> — o bug some no build de produção e volta em desenvolvimento.

Exercício 5

Este é o contador do exemplo completo do artigo. Depois que um sexto <li> entra na lista via JavaScript, o parágrafo passa a mostrar 6? E se a seleção tivesse sido feita com getElementsByTagName, que devolve coleção viva?

const itens = document.querySelectorAll("li");
contador.textContent = `${itens.length} tarefas`; // "5 tarefas"

// mais tarde, em outro ponto do código:
lista.appendChild(document.createElement("li"));
Ver resposta

✓ Resposta: Não, nas duas versões — e por motivos diferentes, o que torna a pergunta útil. Com querySelectorAll, itens é um retrato estático: itens.length continua 5 para sempre. Com getElementsByTagName, itens.length de fato passa a devolver 6 na próxima vez que for lido — mas o parágrafo continua escrito "5 tarefas", porque o texto foi gravado uma vez, com o valor daquele instante, e não existe nenhum vínculo entre a coleção e a string que já está na tela. "Viva" descreve a coleção, nunca o DOM que você já escreveu. A correção é sempre a mesma: extrair uma função atualizarContador() e chamá-la depois de toda alteração na lista. Reconhecer esse trabalho repetitivo — mudou o dado, agora não esqueça de repintar a tela — é reconhecer o problema que React, Vue e companhia existem para resolver.

Comentários

Mais em Javascript

Objetos: estruturando dados do mundo real
Objetos: estruturando dados do mundo real

Atribuir um objeto a outra variável não copia coisa nenhuma: as duas passam a…

Introdução ao TypeScript
Introdução ao TypeScript

Uma variável que é número agora e string depois dá flexibilidade e gera uma…

Projeto Final: Revisão Completa e Aplicação de Produção
Projeto Final: Revisão Completa e Aplicação de Produção

O fim da série reúne tudo numa aplicação de produção: React com rotas e…