¿Qué es el DOM?
Cuando el navegador lee tu HTML, no mantiene una cadena de texto plana. Construye un árbol de objetos llamado Document Object Model. Cada elemento se convierte en un nodo con propiedades y métodos, cada atributo es accesible y cada fragmento de texto tiene su lugar. El DOM es ese árbol, y es dinámico: si lo modificas, la página se actualiza.
Es por esto que JavaScript y HTML se sienten conectados. Tu HTML describe la estructura, el navegador la convierte en objetos y JavaScript manipula esos objetos. El objeto document es el punto de entrada a todo el árbol.
El árbol de nodos
El DOM es jerárquico. El document es la raíz, <html> es su hijo, y todo lo demás cuelga debajo en una relación padre-hijo. Un nodo puede tener muchos hijos, pero solo un padre.
document
└── html
├── head
│ └── title
└── body
├── h1
└── p
Existen varios tipos de nodos. Los nodos de elemento representan las etiquetas. Los nodos de texto contienen los caracteres dentro de los elementos. Los nodos de comentario representan los comentarios. La mayor parte del tiempo trabajarás con elementos, pero saber que existen los nodos de texto explica las sorpresas con los espacios en blanco y por qué childNodes puede incluir más de lo que esperas.
Seleccionando elementos
Antes de poder cambiar cualquier cosa, tienes que encontrarla. La herramienta fundamental hoy en día es querySelector, que acepta cualquier selector de 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 devuelve el primer elemento coincidente o null. querySelectorAll devuelve una NodeList estática de todas las coincidencias, la cual es compatible con forEach pero no se actualiza cuando el DOM cambia. Los métodos más antiguos getElementById, getElementsByClassName y getElementsByTagName siguen funcionando; los dos últimos devuelven colecciones vivas que se actualizan automáticamente.
Lectura y escritura de contenido
Una vez que tienes un elemento, puedes leer o reemplazar su contenido.
// 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
Usa textContent siempre que el valor provenga de un usuario, una API o cualquier lugar que no controles totalmente. Esto trata todo como texto plano y evita la inyección de marcado. innerHTML analiza una cadena como HTML, lo cual es conveniente para plantillas confiables, pero representa un riesgo grave de cross-site scripting con entradas no confiables. Si debes construir marcado a partir de datos, es preferible crear elementos y asignar textContent a cada uno.
Atributos, clases y estilos
Los atributos residen en el elemento y pueden leerse o modificarse directamente.
// 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 es la forma más limpia de gestionar las clases: evita la concatenación de strings y la creación accidental de clases duplicadas. La propiedad style define estilos inline propiedad por propiedad; para cualquier aspecto temático, es preferible alternar una clase y dejar que CSS decida.
Creación, inserción y eliminación de nodos
Construyes nuevos nodos con document.createElement y los ubicas utilizando los métodos de inserción.
// 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 acepta múltiples nodos e incluso cadenas de texto, mientras que insertBefore te permite una ubicación precisa. Cuando necesites insertar muchos nodos, constrúyelos fuera del documento y adjúntalos una sola 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);
Un DocumentFragment es un contenedor ligero que no forma parte de la página. Al adjuntarlo, se mueven todos sus hijos en una sola operación, lo que provoca mucho menos trabajo de layout que añadirlos uno por uno.
Escuchar eventos
Los eventos son la forma en que tu código responde al usuario y al navegador. Adjunta un listener con addEventListener.
// events.js
const button = document.querySelector("#save");
button.addEventListener("click", (event) => {
event.preventDefault();
console.log("clicked", event.target);
});
El objeto del evento contiene información útil: target es el elemento que disparó el evento, currentTarget es el elemento al que está adjunto el listener, y preventDefault() cancela el comportamiento predeterminado del navegador. Algunos eventos comunes incluyen click, input, change, submit, keydown, scroll y DOMContentLoaded.
Delegación de eventos
En lugar de asignar un listener a cada hijo, asigna uno solo al padre e inspecciona event.target. Debido a que los eventos hacen bubble (propagación) desde el objetivo hacia arriba a través de sus ancestros, el padre los detecta todos.
// 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();
});
Esto es más eficiente y, fundamentalmente, sigue funcionando para los elementos añadidos después de haber asignado el listener. Para listas y tablas dinámicas, la delegación es el patrón predeterminado.
Recorriendo el árbol
A veces es necesario moverse de un nodo a sus nodos relacionados.
// 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
Las variantes Element omiten los nodos de texto y de comentario, que es casi siempre lo que se busca. closest es especialmente útil en los manejadores de eventos, como muestra el ejemplo de delegación.
Rendimiento y mejores prácticas
- Agrupa tus lecturas y escrituras. Leer propiedades de diseño como
offsetHeightobliga al navegador a recalcular; intercalar lecturas y escrituras provoca reflows repetidos. - Construye fuera del documento. Utiliza un
DocumentFragmento un elemento fuera de pantalla y adjúntalo una sola vez. - Almacena en caché tus búsquedas. Guarda los elementos en variables en lugar de realizar consultas dentro de un bucle.
- Prefiere las clases sobre los estilos inline. Alterna una clase y mantén la presentación en CSS.
- Usa
textContentpor defecto. UtilizainnerHTMLúnicamente con contenido confiable y sanitizado. - Delega eventos para listas y cualquier elemento dinámico.
- Elimina los listeners que ya no necesites, especialmente en páginas de larga duración, para evitar fugas de memoria.
Errores comunes
- Consultar un elemento antes de que exista y luego preguntarse por qué es
null. - Usar
innerHTMLcon entradas de usuario y abrir un agujero de XSS. - Adjuntar un listener a cada fila en lugar de delegar el evento a la tabla.
- Leer y escribir propiedades de diseño (layout) en el mismo bucle.
- Confundir
event.targetconevent.currentTarget. - Asumir que
querySelectorAlles dinámico cuando es una captura estática.
Próximos pasos
El DOM es el punto de encuentro de todo lo que has aprendido. Combínalo con la sintaxis de ES6+ para escribir un código más limpio, luego aliméntalo con datos mediante la Fetch API y mantenlo robusto utilizando el manejo de errores. La forma más rápida de interiorizarlo es reconstruir algo pequeño —una lista de tareas, una galería filtrable o un buscador en tiempo real— utilizando únicamente vanilla JavaScript.