Pregunta 1En un script cargado en head sin defer, ¿qué devuelve document.querySelector("#total")?
Cómo se ejecuta JavaScript en el navegador
Cómo el navegador arma el DOM a partir del HTML, en qué orden procesa link y script, el atributo defer, la consola de DevTools y cómo revisar resultados como texto en la consola de los ejercicios.
Si cambias el texto de una página con JavaScript, el código puede funcionar o no según dónde lo cargues. Cuando falla, la página no muestra ningún error, así que no sabes si el problema está en el código o en el orden en que se ejecutan las cosas.
En este artículo verás en qué orden el navegador carga el HTML y ejecuta JavaScript.
Los ejemplos de este artículo se abren como páginas HTML
La consola de los ejercicios no puede reproducir el orden en que se carga el HTML, así que este artículo no tiene ejercicios. Cada ejemplo muestra lo que pasa si pones index.html y app.js en la misma carpeta y abres index.html en Chrome; los resultados están en los comentarios al final de cada línea. En la última sección verás cómo revisar los resultados en la consola de los ejercicios.
Del HTML a elementos anidados — el árbol DOM
Imagina que quieres que app.js cambie el texto «Calculando…» de una página de confirmación de pedido por «3,300 yenes». Lo que JavaScript modifica aquí no es el texto del archivo index.html, sino una estructura de datos que el navegador construye al leer el HTML.
El navegador convierte cada elemento del HTML (una parte de la página delimitada por etiquetas, como <p>…</p>) en un objeto, y al conjunto de sus relaciones padre-hijo se le llama DOM (Document Object Model, también llamado árbol DOM; su punto de entrada es document).
Lo que se escribe dentro de una etiqueta, como id="total" o defer, se llama atributo. El id debe ser único: ningún otro elemento de la página puede usar el mismo.
<!-- ---- index.html (página de confirmación del pedido) ---- -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Confirmación del pedido</title>
<link rel="stylesheet" href="style.css"> <!-- CSS que define el aspecto -->
</head>
<body>
<h1>Confirmación del pedido</h1>
<ul id="items"> <!-- tiene dos hijos li -->
<li>Taza × 2</li>
<li>Posavasos × 1</li>
</ul>
<p id="total">Calculando…</p> <!-- el elemento que app.js quiere cambiar -->
</body>
</html>
meta— la codificación es UTF-8title— contiene «Confirmación del pedido»link— apunta a style.css
- Contiene «Confirmación del pedido»
li— contiene «Taza × 2»li— contiene «Posavasos × 1»
- Contiene «Calculando…»
- El elemento que app.js quiere cambiar
app.js obtiene el elemento p con document.querySelector("#total") (busca el elemento cuyo id va después del #, o devuelve null si no existe) y cambia su textContent (el texto que hay dentro del elemento). Como el p está dentro de body, solo se puede encontrar si ya se agregó al DOM.
El HTML se lee de arriba abajo — el orden de link y script
Supón que cargas app.js, que actualiza el total, en <head> junto al CSS. index.html sí contiene <p id="total">, pero al abrir la página sigue diciendo «Calculando…» y el cambio de app.js no tiene efecto.
Durante el análisis del HTML (el navegador lee el HTML desde el principio y va agregando al DOM cada elemento que encuentra), cuando llega a un <link>, sigue analizando mientras descarga el CSS. En cambio, cuando llega a un <script src>, pausa el análisis, descarga el JavaScript y lo ejecuta hasta el final antes de leer lo que sigue.
// ---- index.html (carga app.js en head) ----
// <head>
// <link rel="stylesheet" href="style.css"> ← el análisis continúa
// <script src="app.js"></script> ← el análisis se pausa para ejecutarlo
// </head>
// <body>
// <p id="total">Calculando…</p> ← entra al DOM después de app.js
// </body>
// ---- app.js ----
const total = document.querySelector("#total"); // null (todavía no está en el DOM)
total.textContent = "3,300 yenes";
// TypeError: Cannot set properties of null (setting 'textContent')
El cambio no funcionó porque el p todavía no estaba en el DOM cuando se ejecutó app.js. En la siguiente sección vas a mantener las dos líneas de app.js tal cual y retrasar su ejecución hasta que el DOM esté completo.
Ejecutar cuando el DOM está completo — el atributo defer
Si mueves <script> justo antes de </body>, se ejecuta después de leer el p, así que encuentra #total. Pero entonces que funcione o no depende de dónde esté la etiqueta, y si más adelante vuelves a poner la línea de carga en <head> junto al CSS, obtienes el mismo null otra vez.
El atributo defer (una opción de <script> que descarga el JavaScript sin pausar el análisis del HTML y lo ejecuta cuando el DOM está completo) se escribe <script defer src="app.js"></script> y puede ir en <head>, igual que el <link> del CSS.
// ---- index.html (carga en head con defer) ----
// <head>
// <link rel="stylesheet" href="style.css">
// <script defer src="app.js"></script> ← empieza la descarga, el análisis continúa
// </head>
// <body>
// <p id="total">Calculando…</p> ← app.js se ejecuta tras leer todo hasta </html>
// </body>
// ---- app.js (las mismas 2 líneas de antes, más 1 para comprobar) ----
const total = document.querySelector("#total"); // el elemento p
total.textContent = "3,300 yenes"; // cambia el texto en la página
console.log(total.textContent); // 3,300 yenes
Si hay varios scripts con defer, se ejecutan en el orden en que aparecen en el HTML, no en el orden en que terminan de descargarse. La tabla de abajo muestra, para cada forma de escribir el script, cuándo se ejecuta app.js y si se encuentra #total.
| Cómo se escribe el script | Cuándo se ejecuta app.js | #total |
|---|---|---|
| <script src="app.js"> en head | Al instante, pausando el análisis | No está (null) |
| <script src="app.js"> antes de </body> | Después de leer el p | Está |
| <script defer src="app.js"> en head | Con el DOM completo | Está |
| Código dentro de <script defer> en head | Al instante (sin src, defer no aplica) | No está (null) |
| <script type="module" src="app.js"> en head | Con el DOM completo, sin defer | Está (no si abres el archivo directo) |
Dónde se detuvo y cómo revisar el DOM — DevTools
En la versión sin defer, app.js lanzó un TypeError, pero la página no mostró nada. Si no sabes en qué archivo y en qué línea falló, no puedes averiguar por qué la página se queda en «Calculando…».
DevTools (un panel integrado en el navegador para revisar el DOM y los errores de una página) tiene una pestaña Console, que en Chrome se abre con Ctrl + Shift + J en Windows o Command + Option + J en Mac. Ahí aparece la salida de console.log, junto con los errores y el nombre de archivo y número de línea donde se lanzó cada uno.
// ---- Error en la pestaña Console (haz clic en app.js:2, a la derecha, para abrir esa línea) ----
// Uncaught TypeError: Cannot set properties of null (setting 'textContent')
// at app.js:2:19 ← se lanzó en la línea 2, columna 19
// ---- Con la página ya cargada, escribe esto después del > de Console y pulsa Enter ----
document.querySelector("#total"); // <p id="total">Calculando…</p>
document.querySelector("#total").textContent; // 'Calculando…'
// ---- Línea para agregar después de la línea 1 de app.js (no la escribas en Console) ----
console.log("total al ejecutar:", total); // total al ejecutar: null
Una expresión que escribes en Console se ejecuta sobre el DOM tal como está en ese momento. Para revisar un valor dentro de app.js, agrega un console.log antes de la línea sospechosa; así verás en la pestaña Console el valor que tenía cuando se ejecutó el código.
Revisar el resultado como texto — innerHTML y outerHTML
En los ejercicios de los próximos artículos vas a buscar, cambiar y agregar elementos. Pero la consola de los ejercicios no tiene una página visible y, hasta que recargues, todos los ejercicios comparten una misma página invisible, así que cada ejecución empieza con los elementos que dejó la anterior.
Si asignas un valor a innerHTML (una propiedad que lee y escribe el contenido de un elemento como texto HTML), el contenido se reemplaza por completo. Cada ejercicio empieza asignando al innerHTML de document.body (el elemento body) para borrar los elementos anteriores, y el resultado se revisa imprimiendo outerHTML (el texto HTML que incluye las etiquetas del propio elemento).
// ① Reemplaza todo el contenido de body (aquí se borran los elementos que dejó la ejecución anterior)
document.body.innerHTML = `<p id="total">Calculando…</p>`;
// ② Busca el elemento y cambia su texto
const total = document.querySelector("#total");
total.textContent = "3,300 yenes";
// ③ La página no se ve, así que imprime los resultados como texto
console.log(total.textContent); // 3,300 yenes
console.log(total.outerHTML); // <p id="total">3,300 yenes</p>
console.log(document.body.innerHTML); // <p id="total">3,300 yenes</p>
- Solo vuelve la salida de
console.log, como texto
- Contenido de
document.body— se conserva hasta la siguiente ejecución
const total— solo existe dentro de esta ejecución- Asigna un p a
document.body.innerHTML
const total— se crea como otra variable, distinta de la anterior- La asignación del principio borra el p de la primera ejecución
El código de la consola se ejecuta cada vez dentro de una función nueva, así que const total se crea como una variable distinta de la ejecución anterior y no da un error de redeclaración. Además, se ejecuta cuando la página ya terminó de cargar, así que no tienes que preocuparte por defer.
Si imprimes un elemento directamente, verás {}
La consola de los ejercicios no tiene una página que mostrar y convierte la salida en texto antes de devolverla. Si pasas un elemento tal cual a console.log, aparece como {}, sin nada visible adentro, y no puedes saber si tu cambio funcionó. Para revisar un elemento, imprime su outerHTML o su textContent.
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Con <script defer src="app.js">, ¿cuándo se ejecuta app.js?
Pregunta 3Si ejecutas dos veces document.body.innerHTML = '<p>Calculando…</p>';, ¿cuántos elementos p hay?