O que é o DOM?
Quando o navegador lê o seu HTML, ele não mantém apenas uma string de texto simples. Ele constrói uma árvore de objetos chamada Document Object Model. Cada elemento torna-se um nó com propriedades e métodos, cada atributo fica acessível e cada trecho de texto tem o seu lugar. O DOM é essa árvore, e ela é dinâmica: altere-a e a página será atualizada.
É por isso que o JavaScript e o HTML parecem conectados. O seu HTML descreve a estrutura, o navegador a transforma em objetos e o JavaScript manipula esses objetos. O objeto document é o ponto de entrada para toda a árvore.
A árvore de nós
O DOM é hierárquico. O document é a raiz, o <html> é seu filho, e todo o restante está pendurado abaixo em uma relação de pai e filho. Um nó pode ter muitos filhos, mas apenas um pai.
document
└── html
├── head
│ └── title
└── body
├── h1
└── p
Existem vários tipos de nós. Nós de elemento representam tags. Nós de texto contêm os caracteres dentro dos elementos. Nós de comentário representam comentários. Na maioria das vezes, você trabalhará com elementos, mas saber que os nós de texto existem explica surpresas com espaços em branco e por que o childNodes pode incluir mais do que você espera.
Selecionando elementos
Antes de alterar qualquer coisa, você precisa encontrá-la. A ferramenta principal moderna é querySelector, que aceita qualquer seletor CSS.
// select.js
const title = document.querySelector("h1");
const firstCard = document.querySelector(".card");
const allLinks = document.querySelectorAll("a[target='_blank']");
const byId = document.getElementById("main");
allLinks.forEach((link) => link.classList.add("external"));
querySelector retorna o primeiro elemento correspondente ou null. querySelectorAll retorna uma NodeList estática de todas as correspondências, que suporta forEach, mas não é atualizada quando o DOM muda. Os métodos mais antigos getElementById, getElementsByClassName e getElementsByTagName ainda funcionam; os dois últimos retornam coleções vivas (live collections) que são atualizadas automaticamente.
Lendo e escrevendo conteúdo
Assim que você tiver um elemento, poderá ler ou substituir o conteúdo dele.
// content.js
const heading = document.querySelector("#title");
heading.textContent; // read the visible text
heading.textContent = "Hello"; // replace it, escaped as text
heading.innerHTML = "<em>Hi</em>"; // parses HTML
Use textContent sempre que o valor vier de um usuário, de uma API ou de qualquer lugar que você não controle totalmente. Ele trata tudo como texto simples e não permite a injeção de marcação. innerHTML analisa uma string como HTML, o que é conveniente para templates confiáveis, mas representa um risco sério de cross-site scripting com entradas não confiáveis. Se você precisar construir marcação a partir de dados, prefira criar elementos e definir textContent em cada um.
Atributos, classes e estilos
Os atributos ficam no elemento e podem ser lidos ou alterados diretamente.
// attrs.js
const link = document.querySelector("a");
link.getAttribute("href");
link.setAttribute("href", "/docs");
link.dataset.userId = "42"; // data-user-id="42"
link.classList.add("active");
link.classList.toggle("open");
link.classList.remove("hidden");
link.style.color = "tomato";
classList é a maneira mais limpa de gerenciar classes — isso evita a concatenação de strings e a criação acidental de classes duplicadas. A propriedade style define estilos inline, uma propriedade por vez; para qualquer coisa temática, prefira alternar uma classe e deixar o CSS decidir.
Criando, inserindo e removendo nós
Você cria novos nós com document.createElement e os posiciona utilizando os métodos de inserção.
// build.js
const item = document.createElement("li");
item.textContent = "New task";
item.classList.add("task");
const list = document.querySelector("#todos");
list.append(item); // add at the end
list.prepend(item); // add at the start
item.remove(); // detach from the tree
append aceita múltiplos nós e até strings, enquanto insertBefore oferece um posicionamento preciso. Quando você precisar inserir muitos nós, crie-os fora do documento e anexe-os de uma só vez:
// fragment.js
const fragment = document.createDocumentFragment();
for (const name of names) {
const li = document.createElement("li");
li.textContent = name;
fragment.append(li);
}
list.append(fragment);
Um DocumentFragment é um container leve que não faz parte da página. Anexá-lo move todos os seus filhos em uma única operação, o que dispara muito menos trabalho de layout do que adicioná-los um por um.
Escutando eventos
Eventos são a forma como seu código responde ao usuário e ao navegador. Anexe um listener com addEventListener.
// events.js
const button = document.querySelector("#save");
button.addEventListener("click", (event) => {
event.preventDefault();
console.log("clicked", event.target);
});
O objeto de evento carrega informações úteis: target é o elemento que disparou o evento, currentTarget é o elemento ao qual o listener está anexado, e preventDefault() cancela o comportamento padrão do navegador. Eventos comuns incluem click, input, change, submit, keydown, scroll e DOMContentLoaded.
Delegação de eventos
Em vez de adicionar um listener para cada filho, adicione um ao elemento pai e inspecione event.target. Como os eventos fazem o bubble (propagam-se) do alvo para cima através de seus ancestrais, o pai consegue visualizar todos eles.
// delegate.js
const list = document.querySelector("#todos");
list.addEventListener("click", (event) => {
const button = event.target.closest("button.delete");
if (!button) return;
button.closest("li").remove();
});
Isso é mais eficiente e, crucialmente, continua funcionando para elementos adicionados após o listener ter sido configurado. Para listas e tabelas dinâmicas, a delegação é o padrão recomendado.
Percorrendo a árvore
Às vezes, você precisa se mover de um nó para seus parentes.
// traverse.js
const item = document.querySelector(".item");
item.parentElement; // nearest ancestor element
item.children; // child elements only
item.firstElementChild; // first child element
item.nextElementSibling; // next sibling element
item.closest(".card"); // nearest ancestor matching a selector
As variantes Element ignoram nós de texto e de comentário, que é quase sempre o que você deseja. closest é especialmente útil em manipuladores de eventos, como mostra o exemplo de delegação.
Performance e boas práticas
- Agrupe suas leituras e escritas. Ler propriedades de layout como
offsetHeightforça o navegador a recalcular; intercalar leituras e escritas causa reflows repetidos. - Construa fora do documento. Use um
DocumentFragmentou um elemento fora da tela e, então, anexe-o apenas uma vez. - Faça cache de suas buscas. Armazene elementos em variáveis em vez de realizar queries dentro de um loop.
- Prefira classes a estilos inline. Alterne uma classe e mantenha a apresentação no CSS.
- Use
textContentpor padrão. UseinnerHTMLapenas com conteúdo confiável e sanitizado. - Delegue eventos para listas e qualquer elemento dinâmico.
- Remova listeners que você não precisa mais, especialmente em páginas de longa duração, para evitar leaks.
Erros comuns
- Consultar um elemento antes que ele exista e depois se perguntar por que ele é
null. - Usar
innerHTMLcom entrada de usuário e abrir uma brecha de XSS. - Anexar um listener a cada linha em vez de delegar para a tabela.
- Ler e escrever propriedades de layout no mesmo loop.
- Confundir
event.targetcomevent.currentTarget. - Assumir que
querySelectorAllé live quando, na verdade, é um snapshot estático.
Próximos passos
O DOM é o ponto de encontro de tudo o que você aprendeu. Combine-o com a sintaxe ES6+ para ter um código mais limpo, alimente-o com dados usando a Fetch API e mantenha-o resiliente com tratamento de erros. A maneira mais rápida de internalizar esses conceitos é reconstruir algo pequeno — uma lista de tarefas, uma galeria com filtros ou uma busca em tempo real — inteiramente em vanilla JavaScript.