¿Qué es el DOM? El Plano Vivo
DOM significa Document Object Model. Es la representación en memoria de tu HTML.
Analogía: El Plano de la Casa
Imagina que tu web es una casa.
- HTML: Son los planos originales del arquitecto.
- DOM: Es la casa construida. Si quieres pintar una pared o añadir una ventana después de construirla, no cambias el plano de papel, llamas a un reformista (JavaScript) para que cambie la casa real (DOM).
1. Selección Avanzada: El Francotirador vs La Red
Para cambiar algo, primero tienes que "capturarlo". JavaScript nos da muchas herramientas para esto, y cada una tiene su propósito.
El Especialista: getElementById
¿Cómo funciona? Busca un elemento por su atributo id único.
¿Por qué usarlo? Es el método más rápido y específico. Si sabes el ID, úsalo.
¿Para qué? Para capturar elementos principales y únicos (el header, el menú
principal, el botón de enviar).
let titulo = document.getElementById("titulo_principal");
La Navaja Suiza: querySelector
¿Cómo funciona? Acepta cualquier selector de CSS (clases, IDs, atributos,
combinaciones). Devuelve solo el primer elemento que encuentra.
¿Por qué usarlo? Es extremadamente flexible. Puedes buscar cosas complejas como
.tarjeta > h2.
¿Para qué? Cuando necesitas algo específico que no tiene ID, o cuando solo te
importa el primero de una lista.
let primerBoton = document.querySelector(".btn"); // Solo el primero
let miInput = document.querySelector("input[type='text']"); // Por atributo
La Red de Pesca Moderna: querySelectorAll
¿Cómo funciona? Busca todos los elementos que coincidan con el
selector CSS. Devuelve una NodeList (Lista de Nodos).
¿Por qué usarlo? Te permite capturar grupos enteros de elementos para modificarlos
a la vez.
¿Para qué? Para aplicar cambios a todos los botones, todos los párrafos de una
sección, etc.
let todosLosBotones = document.querySelectorAll(".btn");
Los Clásicos: getElementsByClassName y TagName
Antes de querySelector, usábamos estos.
getElementsByClassName: Busca por clase (sin el punto).
getElementsByTagName: Busca por etiqueta (div, p, button).
¡CUIDADO! Devuelven una HTMLCollection, que es una lista "viva".
Si añades un elemento al DOM, esta lista se actualiza sola mágicamente.
let cajas = document.getElementsByClassName("caja"); // HTMLCollection
2. Manipulación de Contenido: ¿Qué hay dentro?
Una vez seleccionado un elemento, lo más normal es que quieras leer o cambiar lo que tiene escrito. Hay tres formas principales con diferencias clave de seguridad y rendimiento.
innerHTML (El potente/peligroso)
Lee o escribe etiquetas HTML. Si escribes algo, el navegador lo interpreta como código.
caja.innerHTML = "<b>Negrita</b>";
> [!CAUTION] > No lo uses con datos que escriba el usuario (Riesgo XSS).
<script>) en tu web. Si usas
innerHTML con datos externos, el navegador ejecutará el virus del hacker como si
fuera parte de tu código.
textContent (El estándar/seguro)
Lee o escribe texto plano. Es más rápido y seguro porque no interpreta etiquetas.
caja.textContent = "Hola mundo";
> [!TIP] > Es la opción recomendada por defecto para texto.
Ejemplo práctico: Espejo de Texto (Reactividad Básica)
Combina textContent con un evento para ver cómo cambia el DOM en tiempo real:
const input = document.querySelector("#miInput");
const parrafo = document.querySelector("#miParrafo");
input.addEventListener("input", () => {
parrafo.textContent = input.value; // El párrafo "espeja" al input al instante
});
Diferencias en la Lectura (Getters)
Imagina este HTML:
<div>Hola <span style="display:none">Invisible</span></div>
innerHTML: Devuelve"Hola <span style..."(incluye el código HTML).innerText: Devuelve"Hola"(solo lo que el usuario ve realmente, ignora lo oculto por CSS).textContent: Devuelve"Hola Invisible"(toda la información de texto, aunque esté oculta).
// Ejemplo comparativo
let titulo = document.querySelector("h1");
console.log(titulo.innerHTML); // "<span>JS 12</span>: El DOM"
console.log(titulo.innerText); // "JS 12: El DOM"
console.log(titulo.textContent); // "JS 12: El DOM" (pero más rápido)
3. Atributos: Controlando las Etiquetas
Además del contenido, las etiquetas HTML tienen atributos (como src,
href, id, class, disabled...).
Controlarlos es vital para hacer galerías de imágenes, enlaces dinámicos o formularios inteligentes.
Leer Atributos: getAttribute()
Recupera el valor exacto que está escrito en el HTML.
let imagen = document.querySelector("img");
let ruta = imagen.getAttribute("src");
console.log(ruta); // "img/foto.jpg"
Modificar Atributos: setAttribute()
Cambia el valor de un atributo o lo crea si no existe.
let enlace = document.querySelector("a");
// Cambiamos el destino del enlace
enlace.setAttribute("href", "https://google.com");
// Cambiamos el target para que abra en nueva pestaña
enlace.setAttribute("target", "_blank");
Otros Métodos Útiles
hasAttribute(nombre): Devuelvetrueofalsesi el atributo existe.
Ejemplo: Saber si un botón está deshabilitado.removeAttribute(nombre): Borra el atributo por completo.
Ejemplo: Quitardisabledpara reactivar un botón.
🧐 ¿Propiedad vs Atributo?
A veces puedes acceder directamente con el punto (elemento.id,
input.value, img.src).
Esas son propiedades del objeto DOM.
Sin embargo, getAttribute accede al atributo HTML inicial.
Regla general: Para cosas estándar (id, src, value, href) usa el punto por
comodidad.
Para atributos personalizados o cuando el punto no funcione, usa
setAttribute/getAttribute.
4. Iteración: ¿Cómo recorro lo que he capturado?
Una vez tienes una lista de elementos (NodeList o HTMLCollection), no puedes decir
lista.style.color = "red" porque la lista no tiene color. ¡Son los elementos de dentro los
que lo tienen! Tienes que ir uno por uno.
El Problema de las Colecciones
querySelectorAll devuelve un NodeList, que tiene un método
.forEach() incorporado. ¡Genial!
Pero getElementsByClassName devuelve una HTMLCollection, que
NO tiene .forEach().
La Solución Universal: Array.from()
El truco profesional es convertir cualquier lista (sea NodeList o HTMLCollection) en un Array de verdad. Así puedes usar todos los poderes de los Arrays (`map`, `filter`, `forEach`).
// 1. Capturamos (HTMLCollection)
let elementosVivos = document.getElementsByClassName("item");
// 2. Convertimos a Array
let arrayElementos = Array.from(elementosVivos);
// 3. Iteramos (Ahora sí podemos usar forEach)
arrayElementos.forEach(elemento => {
elemento.style.opacity = "0.5";
});
5. Navegación: El Árbol Genealógico
A veces tienes un elemento y quieres encontrar a sus parientes sin volver a buscar en todo el documento.
- Hijo Mayor (Referencia)
- Hijo Mediano
- Hijo Pequeño
let hijo = document.getElementById("hijo_mayor");
// Ir al Padre (parentNode)
// Útil para cerrar un modal cuando haces click en el botón "X" de dentro.
let padre = hijo.parentNode;
// Ir a los Hermanos (nextElementSibling)
// Útil para activar el siguiente campo de un formulario automáticamente.
let hermano = hijo.nextElementSibling;
// Ir a los Hijos (children)
// Útil para limpiar una lista o contar cuántos elementos tiene.
let hijos = padre.children;
Selección Ascendente: closest()
A diferencia de `querySelector` que busca hacia abajo, closest() busca hacia
arriba en el árbol de ancestros hasta encontrar el primer elemento que coincida con el
selector.
// Útil para saber en qué fila de una tabla se hizo click
let fila = e.target.closest("tr");
6. Creación y Destrucción: El Constructor
JavaScript puede crear elementos de la nada e insertarlos en la página. Esto es fundamental para aplicaciones modernas (como un chat o una lista de tareas).
Demo: El Constructor de Ladrillos
Añade ladrillos al muro o haz clic en uno para romperlo.
// 1. Crear el elemento (document.createElement)
// Crea el objeto en memoria, pero aún no es visible en la página.
let nuevoLadrillo = document.createElement("div");
// 2. Configurar (classList, textContent)
// Le damos forma, color y contenido antes de mostrarlo.
nuevoLadrillo.classList.add("ladrillo");
nuevoLadrillo.textContent = "1";
// 3. Insertar en la página (appendChild)
// Ahora sí, lo "pegamos" en el lugar que queramos.
let muro = document.getElementById("zona_construccion");
muro.appendChild(nuevoLadrillo); // Lo pone al final
La Destrucción: remove()
Crear es divertido, pero a veces hay que limpiar. Para borrar un elemento del HTML, tienes el método moderno (el que usarás el 99% de las veces) y el método "padre-hijo".
// 1. El Método Moderno (remove)
// Simple: "¡Tú, fuera!"
let elemento = document.getElementById("ladrillo-feio");
elemento.remove();
// 2. El Método Clásico (removeChild)
// Necesitas el padre para echar al hijo. Es útil si no quieres borrarlo del todo, solo sacarlo.
padre.removeChild(hijo);
🧐 ¿Desaparece para siempre?
Cuando haces remove(), el elemento sale de la página (el usuario deja de verlo).
PERO, si lo tienes guardado en una variable de JS
(let miGuardado = ...),
sigue existiendo en la memoria.
Esto es genial porque puedes guardarlo y volver a hacer appendChild para mostrarlo
más tarde (como en un carrito de compra provisional).
Si quieres borrarlo de la memoria también, haz miGuardado = null.
7. Colocación Precisa: El Bisturí del DOM
appendChild siempre pone el elemento al final. Pero a veces necesitas
ponerlo al principio, o justo antes de otro.
JavaScript moderno tiene métodos muy intuitivos para esto.
Demo: La Lista Dinámica
Usa los botones para insertar elementos en posiciones específicas.
- Item Inicial 1 (Referencia)
- Item Inicial 2 (Referencia)
let padre = document.getElementById("lista");
let nuevo = document.createElement("li");
let referencia = padre.children[0]; // El primer hijo
// 1. Al principio de todo (dentro del padre)
padre.prepend(nuevo);
// 2. Al final de todo (dentro del padre, igual que appendChild)
padre.append(nuevo);
// 3. Justo ANTES de un hermano (fuera de referencia, mismo nivel)
referencia.before(nuevo);
// 4. Justo DESPUÉS de un hermano (fuera de referencia, mismo nivel)
referencia.after(nuevo);
El Potente (y Peligroso): insertAdjacentHTML
A veces no quieres crear elementos con createElement porque es muy tedioso para estructuras
complejas.
insertAdjacentHTML te permite incrustar texto HTML directamente en 4 posiciones.
elemento.insertAdjacentHTML('beforeend', '<div class="alerta">¡Hola!</div>');
// Posiciones: 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
8. Dominando el CSS desde JS
Modificar estilos es fácil, pero leerlos es engañoso. Aquí es donde muchos principiantes fallan.
Demo: El Inspector CSS en Vivo
Este cuadrado tiene estilos definidos en CSS y variables. Intenta cambiarlos desde JS.
Propiedades Computadas (Read-Only)
Modificar (Write)
El Trampa del .style
La propiedad element.style SOLO lee estilos en línea (los que están en el
atributo `style="..."` del HTML).
No ve lo que hay en tu archivo `.css`.
// CSS: .caja { width: 100px; }
let caja = document.querySelector(".caja");
console.log(caja.style.width); // "" (Vacío, porque no está en el HTML)
La Solución: getComputedStyle
Para saber cuánto mide algo de verdad (sumando CSS, herencia, navegador), usa esto. Devuelve los valores finales en píxeles.
let estilos = window.getComputedStyle(caja);
console.log(estilos.width); // "100px" (¡Ahora sí!)
console.log(estilos.backgroundColor); // "rgb(255, 0, 0)" (Siempre devuelve RGB computado)
Variables CSS (Custom Properties)
Lo más moderno es controlar variables CSS desde JS. Así mantienes el diseño en CSS y solo cambias "configuraciones".
// Leer variable
let color = getComputedStyle(elemento).getPropertyValue('--color-principal');
// Escribir variable
elemento.style.setProperty('--color-principal', '#ff0000');
// O borrarla
elemento.style.removeProperty('--color-principal');
Control de Clases: classList Pro
Es la forma recomendada de gestionar el diseño. No toques el `.style` para todo, mejor crea una clase en CSS y quítala/ponla.
// .toggle(clase, fuerza): añade si no está, quita si está.
// El segundo parámetro es un booleano: si es true, SOLO la añade.
elemento.classList.toggle("activo", scoring > 50);
// .replace(vieja, nueva): intercambia una clase por otra.
elemento.classList.replace("modo-claro", "modo-oscuro");
// .contains(clase): devuelve true si la tiene.
if (elemento.classList.contains("bloqueado")) { ... }
9. CSS Avanzado: Inyección y CSSOM
Muchos creen que con JS solo puedes hacer el clásico elemento.style.color = 'red',
pero en realidad tienes el control total sobre las hojas de estilo del documento.
Existen dos formas principales de lograr esto: inyectando una etiqueta <style>
o utilizando la API (Application Programming Interface, que es un conjunto de reglas que permiten que
diferentes aplicaciones o partes de un código se comuniquen entre sí) de CSS Object Model
(CSSOM).
1. El Método Rápido: Inyectar un bloque <style>
Es la forma más sencilla de entender. Creas un elemento de estilo dinámicamente, le metes el texto CSS
que quieras y lo pegas en el <head>.
const style = document.createElement('style');
style.innerHTML = `
.mi-clase-dinamica {
background-color: #3498db;
color: white;
padding: 10px;
border-radius: 8px;
}
#un-id-especifico {
border: 2px dashed red;
}
`;
document.head.appendChild(style);
Ventaja: Es muy legible y puedes escribir CSS tal cual lo conoces.
2. El Método "Pro": CSSOM (insertRule)
Si quieres manipular las hojas de estilo de manera más técnica y eficiente, puedes acceder directamente
al objeto sheet de una hoja de estilo existente o nueva.
// 1. Creamos la hoja de estilo y la añadimos al DOM
const styleEl = document.createElement('style');
document.head.appendChild(styleEl);
// 2. Obtenemos la referencia a la hoja de estilos
const sheet = styleEl.sheet;
// 3. Insertamos reglas individuales
// insertRule(regla, indice)
sheet.insertRule('.nueva-clase { font-size: 20px; color: green; }', 0);
sheet.insertRule('#id-pro { margin-top: 50px; }', 1);
💡 Patrón de Uso: El "Singleton" de Estilos
Tienes razón al sospechar: si ejecutas el código de arriba cada vez que quieras añadir una regla,
acabarás con 100 etiquetas <style> en tu HTML.
Lo ideal es buscar si ya existe nuestra etiqueta "dinámica" antes de crear una nueva.
function agregarReglaDinamica(regla) {
// 1. Intentamos buscar nuestra etiqueta por ID
let styleEl = document.getElementById('mis-estilos-dinamicos');
// 2. Si no existe, la creamos UNA sola vez
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = 'mis-estilos-dinamicos';
document.head.appendChild(styleEl);
}
// 3. Añadimos la regla a la hoja (siempre al principio, índice 0)
styleEl.sheet.insertRule(regla, 0);
}
// Uso:
agregarReglaDinamica('.alerta { color: red; }');
agregarReglaDinamica('body { background: #eee; }'); // Se añade a la MISMA etiqueta
Diferencias Clave
- style.innerHTML: Es como escribir en un archivo de texto. El navegador tiene que "re-parsear" todo el bloque si lo modificas mucho.
- insertRule: Es más rápido para el navegador porque estás interactuando directamente con el modelo de objetos que ya está procesado.
3. ¿Cuándo usar cada uno?
| Situación | Método recomendado |
|---|---|
| Configuración inicial de un tema (Light/Dark mode) | Etiqueta <style> |
| Librerías que generan CSS basado en props (CSS-in-JS) | CSSOM (insertRule) |
| Cambios rápidos y puntuales de un solo elemento | elemento.style |
Ten en cuenta que si creas una regla para una clase que aún no existe en ningún elemento del HTML,
no pasa nada.
En cuanto añadas esa clase a un elemento con
elemento.classList.add('mi-clase-dinamica'), los estilos se aplicarán instantáneamente.
10. Geometría y Posición: getBoundingClientRect()
Si necesitas saber la posición exacta de un elemento en la pantalla (para hacer un menú desplegable, una animación que siga al ratón, o detectar colisiones), este es tu método.
getBoundingClientRect() devuelve un objeto "rect" con las dimensiones y la posición del
elemento relativas al viewport (la parte visible de la ventana).
Analogía: El Radar
Imagina que el navegador es un mapa y cada elemento es un barco.
getBoundingClientRect() es como pedirle al radar las coordenadas exactas de un barco
respecto a tu posición actual.
let caja = document.getElementById("miCaja");
let rect = caja.getBoundingClientRect();
console.log(rect.top); // Distancia desde el borde superior del viewport
console.log(rect.left); // Distancia desde el borde izquierdo del viewport
console.log(rect.width); // Ancho total (incluye padding y border)
console.log(rect.height); // Alto total
console.log(rect.bottom); // top + height
console.log(rect.right); // left + width
⚠️ La Trampa de right y bottom
Aquí es donde casi todo el mundo se equivoca al principio. Los valores de JS no significan lo mismo que las propiedades de CSS.
- En CSS:
right: 20pxsignifica "a 20px del borde derecho". - En JS:
rect.rightsignifica "la distancia desde el borde izquierdo hasta el final del elemento".
Regla mnemotécnica: En JS, top/bottom siempre miden desde
ARRIBA, y left/right siempre miden desde la
IZQUIERDA.
rect.top = 100 para mover el
elemento. Para moverlo, debes usar element.style.top = "100px" combinando con CSS
(posicionamiento absolute/fixed).
¿Viewport o Documento?
Si el usuario hace scroll, los valores de top y left cambiarán porque el
elemento se mueve respecto a la ventana. Si necesitas la posición respecto al inicio de la
página (total), debes sumar el scroll actual:
let posicionRealTop = rect.top + window.scrollY;
let posicionRealLeft = rect.left + window.scrollX;
Ejemplo: Leer, Modificar y Aplicar
Imagina que quieres mover un elemento 50px a la derecha de donde esté ahora mismo.
let caja = document.getElementById("miCaja");
// 1. LEER la posición actual
let rect = caja.getBoundingClientRect();
let actualLeft = rect.left;
// 2. MODIFICAR el valor
let nuevoLeft = actualLeft + 50;
// 3. ESTABLECER (Aplicar) el nuevo valor
// ¡CUIDADO! Para que funcione, el elemento debe tener
// position: absolute o fixed en su CSS.
caja.style.left = nuevoLeft + "px";
Dimensiones de la Ventana: innerWidth e innerHeight
A veces no necesitas medir un elemento, sino saber cuánto espacio total tienes en
pantalla. Para eso usamos las propiedades del objeto window.
window.innerWidth: El ancho total de la parte visible (incluyendo la barra de scroll si existe).window.innerHeight: El alto total de la parte visible.
// Ejemplo 1: Detectar dispositivo móvil por ancho
if (window.innerWidth < 768) {
console.log("Estás en un móvil o tablet.");
}
// Ejemplo 2: Centrar un elemento matemáticamente
let caja = document.getElementById("miCaja");
let rect = caja.getBoundingClientRect();
// (Ancho_ventana / 2) - (Ancho_caja / 2)
let centroX = (window.innerWidth / 2) - (rect.width / 2);
let centroY = (window.innerHeight / 2) - (rect.height / 2);
caja.style.left = centroX + "px";
caja.style.top = centroY + "px";
Responsive JS: matchMedia()
No uses el evento resize para detectar cambios de móvil a escritorio; es muy ineficiente.
Usa matchMedia, que funciona igual que las Media Queries de CSS.
// 1. Definimos la consulta
const queryMovil = window.matchMedia("(max-width: 600px)");
// 2. Escuchamos cambios (Solo se dispara cuando cruza el límite)
queryMovil.addEventListener("change", (e) => {
if (e.matches) {
console.log("Entrando en modo móvil");
} else {
console.log("Volviendo a escritorio");
}
});
Rendimiento: IntersectionObserver
Sirve para detectar cuando un elemento "entra en el campo de visión" (viewport). Es ideal para animaciones al hacer scroll o para cargar imágenes solo cuando se van a ver (Lazy Load).
// 1. Qué hacer cuando se vea
const observador = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
// Dejar de observar si solo quieres que pase una vez
observador.unobserve(entry.target);
}
});
});
// 2. Decirle qué elemento vigilar
const miImagen = document.querySelector(".foto-animada");
observador.observe(miImagen);
> [!TIP]
> **¿Es necesario poner `window.`?** No es estrictamente obligatorio porque `window` es el objeto global en
el navegador. Podrías poner solo `scrollY`. Sin embargo, es una buena práctica usarlo para
dejar claro que estás accediendo a una propiedad del navegador y no a una variable local de tu código.
11. Atributos de Datos (Dataset)
¿Necesitas guardar información invisible en un elemento? Usa data-*. Es el puente estándar
HTML-JS.
Útil para guardar IDs de base de datos, estados, o configuraciones específicas de un elemento.
<div id="producto" data-id="123" data-precio="50">Zapatos</div>
let producto = document.getElementById("producto");
// Leer (dataset.nombrePropiedad - usa camelCase)
// data-id-usuario -> dataset.idUsuario
console.log(producto.dataset.id); // "123"
// Escribir
producto.dataset.precio = "45"; // ¡Rebajado!
12. Errores Comunes y Rendimiento
❌ El Bucle Destructor (Reflow)
for(let i=0; i<100; i++) {
lista.innerHTML += "<li>Item</li>";
}
¡Horror! Esto borra y repinta TODA la lista 100 veces. Es lentísimo y pierde los eventos (onclick) que tuvieran los items antiguos.
✅ DocumentFragment
let fragmento = document.createDocumentFragment();
for(let i=0; i<100; i++) {
let li = document.createElement("li");
fragmento.appendChild(li);
}
lista.appendChild(fragmento);
Prepara todo en memoria (fragmento) y lo pega en el DOM una sola vez. Mucho más rápido.
Peligro de Seguridad: XSS
Nunca uses innerHTML con texto que escriba un usuario. Un usuario malicioso podría
escribir:
<img src=x onerror="robarCookies()">.
Usa siempre textContent o innerText para
texto plano.
Colecciones Vivas (Live Collections)
Las HTMLCollection (de `getElementsByClassName`) cambian solas. Si recorres una lista y
borras elementos a la vez, el índice cambia y te saltarás elementos o darás error.
Regla de oro: Convierte siempre a Array con Array.from() o usa
querySelectorAll (que es estático).
🚀 Bonus: Generar y Descargar Archivos (Desde Cero)
¿Sabías que puedes crear un archivo HTML entero con JavaScript y hacer que el usuario lo descargue? Esto es lo que usan los editores de código modernos.
function descargarHTML() {
const contenido = "<h1>Hola Mundo</h1><p>Generado con JS</p>";
// 1. Crear un Blob (un "bloque" de datos binario)
const blob = new Blob([contenido], { type: 'text/html' });
// 2. Crear una URL temporal para ese bloque
const url = URL.createObjectURL(blob);
// 3. Crear un enlace fantasma, descargarlo y borrarlo
const a = document.createElement('a');
a.href = url;
a.download = "mi_pagina.html";
a.click();
// 4. Limpiar memoria
URL.revokeObjectURL(url);
}
Al pulsar, JavaScript creará un archivo .html en la memoria de tu navegador y lanzará la descarga al instante.
Por motivos de seguridad, JavaScript no puede escribir directamente en una carpeta de tu disco duro (ej: "C:/mis-archivos/") sin que el usuario intervenga.
- En el Navegador: Solo puedes "lanzar" una descarga y que el usuario elija dónde guardarla.
- Node.js: Si ejecutas JS fuera del navegador (como un servidor), sí puedes manipular archivos libremente.
- File System API: Existe una API moderna (usada hoy por reproductores o editores web) que pide permiso para acceder a una carpeta, pero siempre bajo control del usuario.