JavaScript – La etiqueta script

El JavaScript de una página va en etiquetas <script>. Dónde las pongas decide si el código funciona o falla, porque el navegador ejecuta cada script en cuanto lo encuentra, y en ese momento la parte de la página que viene después todavía no existe.

El orden, comprobado

Una página con un script en el <head>, otro al final del <body>, uno con defer y un módulo. Cada uno anota cuándo se ejecuta y si encuentra el párrafo #demo:

<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Orden de los scripts</title>
<script>
  window.registro = [];
  function anota(t) { registro.push(t); }
  anota("1. script normal en <head>: demo = " + document.getElementById("demo"));
</script>
<script src="diferido.js" defer></script>
<script src="modulo.js" type="module"></script>
</head>
<body>
<p id="demo">Párrafo</p>
<script>anota("2. script normal al final del <body>: demo = " + document.getElementById("demo").tagName);</script>
</body>
</html>
salida1. script normal en <head>: demo = null
2. script normal al final del <body>: demo = P
3. defer: se ejecuta tras leer todo el HTML; demo = P
4. type=module: también diferido por omisión; demo = P

El script del <head> busca el párrafo y obtiene null: cuando se ejecuta, el navegador aún no ha llegado a esa línea del HTML. Todos los demás lo encuentran.

Lo que pasa si no lo tienes en cuenta

const boton = document.getElementById("boton");
const demo = document.getElementById("demo");

boton.addEventListener("click", () => {
  demo.textContent = "Cambiando el contenido del párrafo";
});

Este app.js, cargado con un <script src="app.js"> normal en el <head>:

salidaUncaught TypeError: Cannot read properties of null (reading 'addEventListener')

El botón todavía no existe, getElementById devuelve null y la primera línea que lo usa revienta. Es el error más común al empezar con JavaScript.

La solución: defer

<head>
<meta charset="utf-8">
<title>Página de ejemplo</title>
<script src="app.js" defer></script>
</head>
<body>
<p id="demo">Párrafo</p>
<button id="boton" type="button">Cambiar contenido</button>
</body>
el DOM tras pulsar el botón<p id="demo">Cambiando el contenido del párrafo

Con defer, el archivo se descarga a la vez que se lee el HTML, pero no se ejecuta hasta que la página está entera. Así el script puede ir en el <head>, donde se encuentra sin buscarlo, y funciona.

AtributoCuándo se ejecutaOrden
ningunoen cuanto lo encuentra; para la lectura de la página mientrasel del documento
defercon la página entera leídael del documento
asyncen cuanto termina de descargarseel que toque
type="module"como defer, por omisiónel del documento

async es para scripts independientes que no tocan la página ni dependen de otros, como un contador de visitas. Para el código de tu web, defer, o type="module" si usas import.

La versión anterior de esta página llamaba a la función desde el botón con onclick="myFunction()" en el HTML, y además el código HTML de los ejemplos no se veía: las etiquetas no estaban escapadas y el navegador las interpretaba en lugar de mostrarlas. En el ejemplo del archivo externo se definía miFuncion y el botón llamaba a myFunction. Hoy lo habitual es no mezclar JavaScript en el HTML y enlazar los eventos desde el script con addEventListener, como arriba.

Todo lo de esta página se ejecutó en Chromium 154 sin pantalla, en un contenedor limpio; las salidas son las de esa ejecución.