Recorrido del DOM en JavaScript
Recorrer el DOM es una habilidad fundamental para los desarrolladores web. Dominar el recorrido del DOM te permitirá manipular páginas web dinámicamente.
Recorrer el DOM (Document Object Model) es una habilidad fundamental para los desarrolladores web que usan JavaScript. Dominar el recorrido del DOM te permitirá manipular páginas web de forma dinámica, creando experiencias de usuario interactivas y responsivas. Esta guía te proporcionará explicaciones detalladas y múltiples ejemplos de código para ayudarte a dominar el recorrido del DOM.
Introducción al recorrido del DOM
El DOM representa la estructura de una página web como un árbol de nodos. Cada nodo corresponde a un elemento, un fragmento de texto o un comentario en la página. Recorrer el DOM significa moverse de un nodo a otro — hacia arriba hasta un padre, hacia abajo hasta los hijos o lateralmente hacia los hermanos — para leer o modificar elementos en relación con un punto de partida.
¿Por qué recorrer en lugar de simplemente seleccionar? A menudo partes de un elemento que ya tienes (por ejemplo, el botón que el usuario acaba de pulsar) y necesitas llegar a un elemento relacionado cuyo id o clase exactos no conoces de antemano — su contenedor, el siguiente elemento de una lista o todas las respuestas anidadas debajo de él. El recorrido expresa "el elemento que está junto a / dentro de / alrededor de este".
Esta guía cubre las propiedades de relación que se usan con más frecuencia:
| Dirección | Propiedad solo de elementos | Propiedad que incluye todo |
|---|---|---|
| Hacia abajo (hijos) | children, firstElementChild, lastElementChild | childNodes, firstChild, lastChild |
| Hacia arriba (padre) | parentElement | parentNode |
| Lateral (hermanos) | nextElementSibling, previousElementSibling | nextSibling, previousSibling |
La columna de la izquierda omite los nodos de texto y comentarios, por lo que casi siempre es lo que necesitas. La columna de la derecha incluye nodos de texto con espacios en blanco entre etiquetas, lo cual es una fuente habitual de errores.
Para encontrar elementos en cualquier parte del documento (en lugar de relativos a un nodo), consulta Búsqueda: getElement* y querySelector y Selección de elementos del DOM.
Comprender el árbol del DOM
Antes de profundizar en los métodos de recorrido, es útil visualizar el árbol del DOM. Aquí tienes un documento HTML sencillo para ilustrarlo:
<!DOCTYPE html>
<html>
<head>
<title>DOM Traversal Example</title>
</head>
<body>
<div id="container">
<p class="text">Hello, World!</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
</body>
</html>En este documento, el elemento <body> contiene un <div> con un id de container, que a su vez contiene un elemento <p> y un <ul> con hijos <li>. Para aprender cómo el navegador clasifica cada parte (elemento, texto, comentario), consulta Comprensión de los nodos del DOM.
Métodos básicos de recorrido
Acceder a los nodos hijo
Imagina que tienes un blog con varias publicaciones y cada publicación tiene comentarios. Quieres contar los comentarios de una publicación específica.
<!DOCTYPE html>
<html>
<head>
<title>Accessing Child Nodes</title>
</head>
<body>
<div id="blog-post">
<h2>Blog Post Title</h2>
<p>Some interesting content...</p>
<div class="comments">
<p>Comment 1</p>
<p>Comment 2</p>
<p>Comment 3</p>
</div>
</div>
<script>
const commentsContainer = document.querySelector('.comments');
const comments = commentsContainer.children; // Only includes element nodes
// Display the number of comments
console.log(`Number of comments: ${comments.length}`);
</script>
</body>
</html>Este código selecciona el <div> con la clase "comments" y muestra el número de elementos de comentario que contiene. Nota: children devuelve solo nodos de elemento, mientras que childNodes incluye nodos de texto y comentarios. Para el recorrido solo de elementos, prefiere children.
Navegar hacia los nodos padre
Imagina que tienes una lista de artículos en un carrito de compras y quieres encontrar el elemento contenedor de un artículo específico.
<!DOCTYPE html>
<html>
<head>
<title>Navigating to Parent Nodes</title>
</head>
<body>
<div id="shopping-cart">
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
<script>
const cartItem = document.querySelector('li');
const parent = cartItem.parentNode;
// Display the parent node
console.log(`The parent of the first cart item is a: ${parent.tagName}`);
</script>
</body>
</html>Este código selecciona el primer elemento <li> y muestra el nombre de la etiqueta de su nodo padre. Para la navegación solo de elementos, parentElement se prefiere con frecuencia sobre parentNode, ya que omite los nodos de texto y devuelve null si el padre no es un elemento.
Nodos hermanos
Imagina que tienes una lista de tareas donde puedes marcarlas como completadas y luego pasar a la siguiente.
<!DOCTYPE html>
<html>
<head>
<title>Task List Navigation</title>
<style>
.task {
margin: 10px;
padding: 10px;
border: 1px solid #ccc;
}
.completed {
text-decoration: line-through;
color: gray;
}
</style>
</head>
<body>
<div class="task-list">
<div class="task">
<p>Task 1: Do the laundry</p>
<button class="complete-task">Complete Task</button>
</div>
<div class="task">
<p>Task 2: Buy groceries</p>
<button class="complete-task">Complete Task</button>
</div>
<div class="task">
<p>Task 3: Clean the house</p>
<button class="complete-task">Complete Task</button>
</div>
</div>
<script>
document.querySelectorAll('.complete-task').forEach(button => {
button.addEventListener('click', () => {
const task = button.parentElement;
task.classList.add('completed');
button.disabled = true;
const nextTask = task.nextElementSibling;
if (nextTask) {
console.log(`Next task: ${nextTask.querySelector('p').textContent}`);
} else {
console.log('No more tasks available');
}
});
});
</script>
</body>
</html>Este código proporciona una lista de tareas donde cada tarea tiene un botón "Complete Task". Cuando una tarea se marca como completada, el texto aparece tachado y el botón se deshabilita. También muestra la descripción de la siguiente tarea. Si no hay más tareas, indica que no hay más disponibles. De forma similar, previousElementSibling y nextElementSibling omiten los nodos de texto, lo que los hace más seguros para el recorrido solo de elementos que previousSibling y nextSibling.
Técnicas avanzadas de recorrido
Buscar elementos por clase o etiqueta
Imagina que estás creando un panel que lista todos los usuarios y quieres encontrar y contar todos los elementos de usuario.
<!DOCTYPE html>
<html>
<head>
<title>Finding Elements by Class or Tag</title>
</head>
<body>
<div class="user">User 1</div>
<div class="user">User 2</div>
<div class="user">User 3</div>
<script>
const users = document.getElementsByClassName('user');
// Display the number of users
console.log(`Number of users: ${users.length}`);
</script>
</body>
</html>Este código cuenta y muestra el número de elementos con la clase user. Un detalle sutil pero importante: getElementsByClassName (y getElementsByTagName) devuelve un HTMLCollection en vivo — se actualiza automáticamente a medida que cambia el DOM. Si agregas un cuarto elemento .user más tarde, users.length se convierte en 4 sin necesidad de volver a consultar. querySelectorAll, por el contrario, devuelve un NodeList estático que es una instantánea tomada en el momento de la llamada. Compara ambos en Búsqueda: getElement* y querySelector.
Métodos de querySelector
Imagina que tienes un sitio de noticias y quieres resaltar todos los titulares.
<!DOCTYPE html>
<html>
<head>
<title>Query Selector Methods</title>
</head>
<body>
<div id="news">
<h1 class="headline">Headline 1</h1>
<h1 class="headline">Headline 2</h1>
<h1 class="headline">Headline 3</h1>
</div>
<script>
const headlines = document.querySelectorAll('.headline');
// Highlight all headlines
headlines.forEach(headline => {
headline.style.color = 'red';
});
// Display the number of headlines
console.log(`Number of headlines: ${headlines.length}`);
</script>
</body>
</html>Este código selecciona todos los elementos con la clase headline, cambia su color a rojo y muestra el recuento de estos elementos.
Recorrido con funciones recursivas
Creemos un ejemplo del mundo real para el recorrido recursivo. Usaremos un sistema de comentarios anidados como ejemplo, donde cada comentario puede tener respuestas.
<!DOCTYPE html>
<html>
<head>
<title>Recursive Traversal</title>
<style>
.comment {
margin: 10px;
padding: 10px;
border: 1px solid #ccc;
}
.reply {
margin-left: 20px;
border-left: 2px solid #aaa;
}
</style>
</head>
<body>
<div class="comments">
<div class="comment">
<p>Comment 1</p>
<div class="reply">
<p>Reply 1-1</p>
<div class="reply">
<p>Reply 1-1-1</p>
</div>
</div>
<div class="reply">
<p>Reply 1-2</p>
</div>
</div>
<div class="comment">
<p>Comment 2</p>
<div class="reply">
<p>Reply 2-1</p>
</div>
</div>
</div>
<script>
function traverseComments(node) {
if (!node) return; // Guard against null/undefined
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('comment')) {
console.log(`Comment: ${node.querySelector('p').textContent}`);
}
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('reply')) {
console.log(`Reply: ${node.querySelector('p').textContent}`);
}
for (let i = 0; i < node.childNodes.length; i++) {
traverseComments(node.childNodes[i]);
}
}
traverseComments(document.querySelector('.comments'));
</script>
</body>
</html>Este código representa un sistema de comentarios anidados con comentarios y respuestas. La función traverseComments recorre de forma recursiva cada comentario y respuesta, mostrando su contenido de texto. La estructura anidada permite respuestas a respuestas, lo que demuestra un caso de uso del mundo real del recorrido recursivo. Incluye siempre una comprobación de null/undefined al inicio de las funciones recursivas para evitar errores cuando el selector inicial no devuelve nada.
Ejemplos prácticos
Estos ejemplos combinan el recorrido del DOM con técnicas comunes de manipulación para demostrar flujos de trabajo del mundo real.
Crear una lista de tareas dinámica
Imagina que tienes una lista de tareas donde los usuarios pueden agregar nuevas tareas.
<!DOCTYPE html>
<html>
<head>
<title>Dynamic To-Do List</title>
<style>
.info { color: darkgreen; }
</style>
</head>
<body>
<div id="todo-list">
<h2>To-Do List</h2>
<ul id="tasks">
<li>Task 1</li>
<li>Task 2</li>
</ul>
<input type="text" id="task-input" placeholder="Add a new task" />
<button id="add-button">Add Task</button>
</div>
<script>
const tasks = document.getElementById('tasks');
const input = document.getElementById('task-input');
const button = document.getElementById('add-button');
button.addEventListener('click', () => {
const newTask = input.value.trim();
if (newTask) {
const li = document.createElement('li');
li.textContent = newTask;
tasks.appendChild(li);
input.value = '';
console.log('Added new task to the to-do list');
}
});
</script>
</body>
</html>Este código permite a los usuarios agregar nuevas tareas a una lista de tareas escribiendo texto en un campo de entrada y haciendo clic en un botón.
Actualizar atributos de elementos
Imagina que tienes una lista de productos y quieres marcarlos como "favorito" cuando se hace clic en ellos.
<!DOCTYPE html>
<html>
<head>
<title>Updating Element Attributes</title>
<style>
.favorite { font-weight: bold; color: gold; }
.info { color: darkblue; }
</style>
</head>
<body>
<h4>Click on the list item below to see the result!</h4>
<ul id="product-list">
<li>Product 1</li>
<li>Product 2</li>
<li>Product 3</li>
</ul>
<script>
const productList = document.getElementById('product-list');
productList.addEventListener('click', (event) => {
if (event.target.tagName === 'LI') {
event.target.classList.toggle('favorite');
console.log(`Toggled favorite status for: ${event.target.textContent}`);
}
});
</script>
</body>
</html>Este código permite a los usuarios marcar productos como "favorito" haciendo clic en ellos, cambiando su apariencia mediante una clase favorite.
Minimiza el acceso al DOM para mejorar el rendimiento. Agrupa las manipulaciones del DOM para reducir los reflujos y repintados.
Errores comunes
Algunos problemas recurrentes son responsables de la mayoría de los errores en el recorrido del DOM:
- Nodos de texto con espacios en blanco.
firstChild,nextSiblingychildNodescuentan los espacios en blanco y los saltos de línea entre etiquetas como nodos de texto. El primer hijo de un<ul>escrito en varias líneas suele ser un nodo de texto, no el primer<li>. Usa las versiones solo de elementos (firstElementChild,nextElementSibling,children) a menos que necesites específicamente nodos de texto. - Olvidar que el recorrido puede devolver
null.parentElement,nextElementSiblingy similares devuelvennullen los bordes del árbol (el último hermano no tienenextElementSibling). Comprueba siempre antes de llamar a un método sobre el resultado, como hace el ejemplo de hermanos conif (nextTask). - Tratar colecciones como arrays.
children,childNodesygetElementsByClassNamedevuelven colecciones, no arrays reales.HTMLCollectionno tieneforEach. Convierte conArray.from(collection)o[...collection]cuando necesites métodos de array comomapofilter. (ElNodeListdequerySelectorAllsí tieneforEach, pero nomap.) - Iterar sobre una colección en vivo mientras se modifica. Como
getElementsByClassNamees en vivo, agregar o eliminar elementos coincidentes dentro de un bucleforsobre ella puede saltar elementos o entrar en un bucle infinito. Toma una instantánea primero conArray.from(...)si planeas mutar durante la iteración.
Para profundizar en cómo se tipifican los nodos y cómo se ven sus contenidos, lee Propiedades de nodo: tipo, etiqueta y contenidos. Para responder a las acciones del usuario mientras recorres el DOM, consulta Manejo de eventos en el DOM.
Conclusión
Dominar el recorrido del DOM es esencial para crear aplicaciones web dinámicas e interactivas. Al comprender y utilizar los diversos métodos y técnicas para navegar y manipular el DOM, puedes mejorar la experiencia del usuario y la funcionalidad de tus proyectos web.