Skip to content
This page has been auto-translated and may contain errors.View in English

El DOM

docs.scrimba.com

Tu archivo HTML se carga una vez, y luego la página se queda allí. Pero un cuadro de búsqueda que sugiere resultados mientras escribes, un menú que se abre al hacer clic, un contador que aumenta, nada de eso está en el archivo HTML. Algo cambia lo que hay en la página después de que ha cargado, y ese algo es JavaScript llegando a la página a través del DOM. Este capítulo trata sobre ese alcance: cómo JavaScript encuentra las partes de una página y las cambia para que la página se actualice frente al lector.

Qué es el DOM

Cuando el navegador carga tu página, lee tu HTML y construye un árbol de objetos a partir de él: un objeto para la página, uno para <body>, uno para cada encabezado, párrafo y botón adentro. Ese árbol es el DOM, abreviatura de Document Object Model. Tu JavaScript puede leer este árbol y cambiarlo, y cuando lo hace, la página que ves se actualiza de inmediato.

La parte que vale la pena recordar: el DOM no es tu archivo HTML. Es la versión viva de la página, sentada en la memoria del navegador. Tu archivo .html es la receta inicial; el DOM es el plato terminado que el navegador está sirviendo, y JavaScript puede reorganizar el plato mientras todos todavía están comiendo.

El DOM (Document Object Model) es el árbol en memoria que el navegador construye a partir de tu HTML. Cada elemento se convierte en un objeto, anidado de la forma en que tus etiquetas están anidadas: <body> contiene <main>, que contiene <section>, que contiene un párrafo. JavaScript obtiene acceso a este árbol a través del document global, y desde allí puede recorrer el árbol, leer cualquier elemento y cambiarlo.

La distinción que evita confusión más adelante es que el DOM es separado de tu HTML fuente. El HTML se analiza una vez para construir el árbol; después de eso, el árbol es lo que está activo. Cambia el DOM y la página se actualiza; tu archivo HTML original en el disco nunca cambia. Por eso "ver fuente" puede verse completamente diferente a lo que ves en pantalla una vez que JavaScript ha ejecutado.

El DOM es el modelo de objeto vivo del navegador del documento: un árbol de nodos (elementos, texto, comentarios, y el documento en sí) que el navegador construye al analizar tu HTML, y desde el que lo renderiza. Es una API independiente del lenguaje, expuesta a JavaScript a través de document y los objetos que cuelgan de él, pero no es parte de JavaScript el lenguaje; es una característica del navegador que proporciona el tiempo de ejecución.

El marco que importa en la práctica: el HTML fuente es una entrada, analizada una sola vez en el árbol, y el DOM es la fuente de verdad desde entonces. Lee document.body.innerHTML después de que ejecuten tus scripts y obtienes el árbol serializado actual, no los bytes que enviaste. Esto también es por qué el DOM viene directamente de HTML: las etiquetas que escribes definen el árbol inicial, y todo lo que JavaScript hace es editar ese árbol después. Manipular el DOM tiene un costo que la fuente no tiene, porque los cambios pueden activar que el navegador recompute el diseño y repinte, que es el hilo que sigues en las secciones avanzadas a continuación.

Cada elemento en el DOM es un objeto, del mismo tipo que encontraste en objetos, con propiedades que puedes leer y cambiar. Ese es el truco completo: porque la página es objetos, y ya sabes cómo trabajar con objetos, cambiar la página es cambiar propiedades de objetos.

JunoQué es el DOM El navegador convierte tu HTML en un árbol de objetos llamado DOM, y ese árbol es la página viva. Tu archivo HTML es la receta; el DOM es la comida que se está sirviendo, y JavaScript puede cambiarla mientras está en la mesa. Cada elemento es un objeto, así que cambiar la página significa cambiar propiedades de objetos.
JunoQué es el DOM El DOM es el árbol en memoria que el navegador construye a partir de tu HTML, al que se accede a través de document. Es separado de tu archivo fuente: se analiza una sola vez, luego el árbol es lo que está activo, por eso "ver fuente" puede diferir de lo que ves. Cambia el árbol y la página se actualiza.
JunoQué es el DOM El DOM es un árbol de nodos que el navegador analiza desde HTML y desde el que renderiza; es una API del navegador, no parte del lenguaje. La fuente es una entrada analizada una sola vez, el árbol es la verdad después de eso, así que innerHTML refleja el árbol actual, no tus bytes enviados. Las ediciones pueden costar reflow y repaint, por eso los patrones de escritura importan más adelante.

Seleccionar elementos

Antes de poder cambiar un elemento, tienes que encontrarlo en el árbol. Lo señalas de la misma manera que CSS, con un selector, y el navegador te devuelve el objeto coincidente.

El caballo de batalla es document.querySelector. Le das un selector CSS, el mismo tipo de patrón que usas en CSS, y te devuelve el primer elemento que coincide:

js
const card = document.querySelector(".card"); // primer elemento con class="card"
const total = document.querySelector("#total"); // el elemento con id="total"

.card coincide por clase, #total coincide por id, y una palabra simple como "button" coincide por nombre de etiqueta. querySelector te da la primera coincidencia y se detiene allí. Cuando quieres cada coincidencia en su lugar, usa querySelectorAll, que devuelve una lista que puedes recorrer:

js
const cards = document.querySelectorAll(".card"); // todos los elementos con class="card"
console.log(cards.length); // cuántos encontró

Si nada coincide, querySelector devuelve null, que es la forma de JavaScript de decir "no hay nada aquí".

document.querySelector toma cualquier selector CSS y devuelve el primer elemento coincidente, o null si ninguno coincide. document.querySelectorAll devuelve todas las coincidencias como una lista:

js
const toggle = document.querySelector(".menu-toggle"); // primera coincidencia, o null
const items = document.querySelectorAll(".card"); // cada coincidencia, como una lista

Los selectores son CSS completo, no solo clase e id: querySelector("nav a") encuentra el primer enlace dentro de un nav, querySelector("input[type='email']") coincide por atributo. Lo que lo selecciona en una hoja de estilos lo selecciona aquí.

El retorno null es el que debes planificar. Como una falta es null, código como document.querySelector(".missing").textContent lanza, ya que no puedes leer una propiedad de null. Cuando un elemento podría no existir, verifica primero: if (toggle) { ... }.

document.querySelector(selector) devuelve el primer elemento que coincide con un selector CSS en el orden del documento, o null; document.querySelectorAll(selector) devuelve una NodeList de todas las coincidencias. Ambas aceptan la gramática completa del selector CSS, así que combinadores, selectores de atributo y pseudoclases funcionan todos:

js
const firstError = document.querySelector(".form .field.is-invalid input");
const externalLinks = document.querySelectorAll("a[href^='http']");

Dos propiedades del resultado vale la pena conocer. Primero, la NodeList de querySelectorAll es estática: es una instantánea tomada en el momento de la llamada, así que los elementos agregados a la página después no aparecen en ella. Eso es diferente de las APIs más antiguas (getElementsByClassName, getElementsByTagName), que devuelven una viva HTMLCollection que se actualiza a sí misma a medida que cambia el DOM; más sobre esa diferencia en la última sección. Segundo, el alcance importa para el rendimiento y la corrección: llamar a querySelector en un elemento en lugar de document busca solo el subárbol de ese elemento (card.querySelector(".title")), que es más rápido en páginas grandes e inmune a coincidir algo en otro lugar. Recurre a la forma con alcance siempre que ya tengas el contenedor.

JunoSeleccionar elementos Para encontrar un elemento, pásale a document.querySelector un selector CSS como .card o #total, y te da la primera coincidencia. Usa querySelectorAll cuando quieras todas como una lista. Si nada coincide obtienes null, que significa "nada aquí".
JunoSeleccionar elementosquerySelector devuelve la primera coincidencia, querySelectorAll devuelve cada coincidencia como una lista, y ambas toman cualquier selector CSS que usarías en una hoja de estilos. Una falta devuelve null, así que leer una propiedad de él lanza; protege con if (element) cuando el elemento podría no estar allí.
JunoSeleccionar elementos Ambos métodos toman selectores CSS completos; querySelectorAll devuelve una NodeList estática, una instantánea que no verá adiciones posteriores. Llama a querySelector en un elemento, no en document, para buscar solo su subárbol, que es más rápido y evita coincidir algo en la página. Y recuerda que una falta es null.

Leer y cambiar elementos

Una vez que tienes un elemento, lo lees y cambias a través de sus propiedades, de la misma manera que lees y cambias cualquier objeto. Aquí es donde la página realmente comienza a moverse.

La propiedad que usarás más es textContent, el texto dentro de un elemento. Puedes leerlo o configurarlo:

js
const total = document.querySelector("#total");
total.textContent = "42 elementos"; // la página ahora muestra "42 elementos"

Para cambiar cómo se ve algo, usualmente añades o eliminas una clase y dejas que CSS haga el estilo. classList tiene tres métodos para eso:

js
const card = document.querySelector(".card");
card.classList.add("selected"); // añadir una clase
card.classList.remove("selected"); // quitarla
card.classList.toggle("selected"); // añadir si falta, quitar si presente

También puedes configurar un estilo único directamente a través de .style, y establecer un atributo como src o href con setAttribute:

js
card.style.color = "crimson"; // un estilo en línea
const avatar = document.querySelector(".avatar");
avatar.setAttribute("src", "maría.jpg"); // establecer la fuente de imagen

Recurre a classList primero y .style solo para ajustes puntuales, porque mantener tu estilo en CSS lo mantiene en un solo lugar.

textContent lee y escribe el texto dentro de un elemento. classList alterna clases para que el estilo se mantenga en tu CSS. .style establece estilos en línea individuales, y setAttribute establece cualquier atributo:

js
const badge = document.querySelector(".badge");
badge.textContent = `${count} nuevo`; // establecer el texto
badge.classList.toggle("is-hidden", count === 0); // alternar con una condición
badge.setAttribute("aria-label", `${count} mensajes nuevos`); // establecer un atributo

classList.toggle toma un argumento segundo opcional: un booleano que fuerza la clase encendida cuando es true y apagada cuando es false, que es más limpio que un if/else alrededor de add y remove.

La precaución que vale la pena declarar temprano: también está innerHTML, que establece el contenido de un elemento como HTML puro, etiquetas y todo. Es tentador porque parece flexible, pero usa textContent, no innerHTML, para cualquier texto que vino de un usuario. Establecer innerHTML desde entrada del usuario permite que esa entrada inyecte HTML real en tu página, que es una brecha de seguridad. Cuando estés configurando texto simple, textContent es la herramienta correcta de todas formas.

La superficie de lectura y escritura es amplia, pero cuatro propiedades llevan la mayoría del trabajo: textContent para texto, classList para clases de estado, .style para estilos únicos calculados, y setAttribute para atributos. Prefiere classList sobre .style para que la presentación viva en CSS y JavaScript solo voltee clases de estado (clases como is-open o is-loading que tu hoja de estilos luego estila):

js
const panel = document.querySelector(".panel");
panel.classList.toggle("is-open", shouldOpen);
panel.setAttribute("aria-expanded", String(shouldOpen)); // los atributos son cadenas

El borde afilado es innerHTML. Configurarlo analiza la cadena como HTML y construye nodos reales a partir de ella, así que cualquier marcado en esa cadena se vuelve activo: un <script> o un manejador onerror en texto suministrado por el usuario se ejecuta en tu página. Eso es cross-site scripting (XSS): inyectar contenido ejecutable a través de datos que trataste como texto. textContent nunca analiza HTML; establece texto y solo texto, así que es el defecto seguro y el más rápido, ya que el navegador omite el análisis HTML. Restringe innerHTML al marcado que controlas completamente, y nunca lo construyas concatenando entrada del usuario. Cuando necesites insertar datos del usuario en marcado estructurado, establece la estructura con métodos seguros y deja caer el valor de confianza en a través de textContent, que es el patrón que la siguiente sección desarrolla.

Una nota más sobre atributos: setAttribute escribe el atributo HTML (una cadena), mientras que muchos elementos también exponen una propiedad viva que puede diferir. input.value refleja lo que el usuario ha escrito justo ahora; input.getAttribute("value") devuelve el valor predeterminado original del marcado. Para campos de formulario, lee la propiedad, no el atributo.

JunoLeer y cambiar elementos Establece el texto dentro de un elemento con textContent, y cambia su apariencia añadiendo o eliminando una clase con classList.add, remove, y toggle. Usa .style para un ajuste puntual y setAttribute para cosas como una fuente de imagen. Apóyate en clases para que tu estilo se mantenga en tu CSS.
JunoLeer y cambiar elementostextContent para texto, classList para clases de estado, .style para puntuales, setAttribute para atributos; classList.toggle incluso toma un booleano para forzarlo encendido o apagado. Mantente alejado de innerHTML para cualquier cosa que un usuario escribió, ya que inyecta HTML real en tu página. Para texto simple, textContent es la herramienta correcta de todas formas.
JunoLeer y cambiar elementos Voltea clases de estado con classList y mantén el estilo en CSS; usa setAttribute para atributos, recordando que son cadenas. innerHTML analiza su cadena como HTML vivo, así que datos del usuario en él es un agujero XSS; textContent nunca analiza y es tanto seguro como más rápido. Y para campos de formulario lee la propiedad value viva, no el atributo.

Crear y eliminar elementos

Cambiar elementos que ya existen te lleva lejos, pero a veces no hay nada allí todavía: un pendiente que estás añadiendo, un resultado de búsqueda llegando. Para eso construyes nuevos elementos y los pones en el árbol, y sacas los viejos.

Haces un nuevo elemento con document.createElement, le das algo de contenido, luego lo añades a la página con append:

js
const list = document.querySelector(".todo-list");
const item = document.createElement("li"); // un nuevo <li>, no en la página aún
item.textContent = "Comprar leche de avena"; // darle algo de texto
list.append(item); // ahora aparece, al final de la lista

Un nuevo elemento de createElement existe solo en memoria hasta que lo append en algún lugar; ese es el paso que lo pone en la página. Para sacar un elemento de la página, llama a remove en él:

js
item.remove(); // desaparecido de la página

document.createElement(tag) construye un elemento, append lo añade como el último hijo de un padre, y remove saca un elemento del árbol:

js
const list = document.querySelector(".todo-list");
const item = document.createElement("li");
item.textContent = "Comprar leche de avena"; // seguro: texto, no analizado como HTML
item.classList.add("todo-item");
list.append(item); // adjuntarlo a la página

Establece el texto con textContent antes de añadir, no construyendo una cadena HTML, así que el texto ingresado por el usuario nunca puede inyectar marcado. append también acepta cadenas simples y varios nodos a la vez (list.append(item, "o", another)), que es práctico para contenido mixto.

Un detalle que atrapa a la gente: crear un elemento no hace nada visible por sí solo. Se queda en memoria hasta que lo adjuntes a algo ya en el árbol. Hasta entonces es real pero fuera de pantalla, que es exactamente la propiedad que la sección avanzada usa para construir eficientemente.

createElement construye un nodo desconectado, append inserta nodos (o cadenas) como los últimos hijos de un padre, y remove desconecta un nodo del árbol. La pregunta de eficiencia es la interesante, porque insertar en el árbol vivo puede costar.

Cada vez que cambias el DOM de una manera que afecte la geometría, el navegador podría necesitar recomputar posiciones y tamaños (reflow, también llamado layout) y luego repintar. Haz eso dentro de un bucle, añadiendo un nodo por iteración a la página viva, y puedes activar ese trabajo repetidamente. La solución es construir fuera del DOM e insertar una vez. Un DocumentFragment es un contenedor ligero, fuera de pantalla para exactamente esto: añades nodos en el fragmento (que no está en el árbol vivo, así que sin reflow), luego añades el fragmento una vez, que mueve sus hijos como una operación única:

js
const list = document.querySelector(".results");
const fragment = document.createDocumentFragment(); // contenedor fuera de pantalla

for (const name of names) {
  const li = document.createElement("li");
  li.textContent = name; // inserción segura de datos del usuario
  fragment.append(li); // sin reflow: fragmento está fuera del DOM
}

list.append(fragment); // una inserción en el árbol vivo

Añadir el fragmento lo vacía en el padre, así que el fragmento en sí no aparece en el DOM; solo sus hijos lo hacen. El mismo principio impulsa un hábito más amplio: escrituras DOM en lote. Leer una propiedad de layout (como offsetHeight) fuerza al navegador a descargar layout pendiente, así que intercalar lecturas y escrituras en un bucle causa layout thrash, reflows forzados repetidos. Agrupa tus lecturas, luego agrupa tus escrituras. Para un puñado de nodos nada de esto importa; para cientos es la diferencia entre suave e entrecortado.

JunoCrear y eliminar elementos Construye un nuevo elemento con document.createElement, dale texto con textContent, y ponlo en la página con append. Un elemento fresco vive solo en memoria hasta que lo añadas, así que ese paso es lo que lo hace aparecer. Para sacarlo de la página, llama a remove en él.
JunoCrear y eliminar elementoscreateElement lo construye, append lo adjunta como el último hijo, remove lo saca. Establece texto con textContent antes de añadir en lugar de construir una cadena HTML, así que nada que un usuario escribió puede inyectar marcado. Recuerda que un elemento creado permanece invisible hasta que lo adjuntes a algo ya en la página.
JunoCrear y eliminar elementos Añadir al árbol vivo en un bucle puede activar reflow cada vez; construye en un DocumentFragment fuera del DOM y añádelo una vez para que los hijos se muevan juntos. Leer una propiedad de layout a mitad del bucle fuerza layout y causa thrash, así que agrupa tus lecturas y luego tus escrituras. Para pocos nodos es discutible; para cientos es suave contra entrecortado.

Por dónde continúa el DOM

Seleccionar, leer, cambiar, crear y eliminar cubren la mayoría de lo que JavaScript hace en una página, y todo es el mismo movimiento por debajo: encontrar un objeto en el árbol y cambiar sus propiedades. Porque el DOM es objetos, todo lo que aprendiste en objetos se aplica directamente aquí, y el pequeño conjunto de métodos en este capítulo (querySelector, textContent, classList, createElement, append) es la mayoría del kit de herramientas.

Lo que falta hasta ahora es la sincronización. Cada ejemplo aquí se ejecuta una sola vez, de arriba a abajo, pero una página real cambia en respuesta a lo que hace el lector: un clic, una pulsación de tecla, un formulario enviado. Conectar tus cambios de DOM a esos momentos es eventos, el siguiente capítulo, donde addEventListener conecta "el usuario hizo esto" a "así que la página hace aquello". Una vez que seleccionar y cambiar se junten con responder a eventos, puedes construir las páginas interactivas en las que se enfoca el resto de este manual.