JavaScript: el comportamiento de la página¶
Con HTML y CSS tenemos una página que informa y que se ve bien, pero que sigue siendo un documento: no reacciona. Si el usuario hace clic, escribe en un campo o envía un formulario, nada cambia hasta que el servidor entrega una página nueva.
JavaScript es la capa que completa el modelo:
| Capa | Responsabilidad |
|---|---|
| HTML | Contenido |
| CSS | Presentación |
| JavaScript | Comportamiento |
Con JS una página pasa de ser estática a ser interactiva: puede responder a eventos del usuario (clics, teclado, formularios), modificar el contenido en tiempo real a través del DOM, validar datos antes de enviarlos al servidor y consumir APIs REST.
Esta última capacidad es la que conecta este módulo con el resto del curso. Cuando lleguemos a Angular, seguiremos escribiendo JavaScript —con más estructura y herramientas, pero el lenguaje es el mismo—.
¿Dónde se ejecuta JavaScript?¶
En desarrollo web, JS se ejecuta principalmente en el navegador (frontend). El navegador interpreta el código y lo corre en su motor —V8 en Chrome, SpiderMonkey en Firefox—, lo que permite:
- Reaccionar a eventos del usuario.
- Manipular HTML y CSS a través del DOM.
- Validar formularios en el cliente.
- Consumir APIs mediante HTTP.
JavaScript también corre en el servidor
Existe Node.js, que ejecuta JavaScript fuera del navegador y permite construir backends completos. En este módulo nos enfocamos en JS en el navegador; nuestro backend será Spring Boot.
Validar en el cliente no es suficiente
Las validaciones en JavaScript mejoran la experiencia: el usuario ve el error de inmediato, sin esperar una respuesta del servidor. Pero cualquiera puede desactivar JavaScript o enviar la petición directamente con Postman. Por eso, toda validación crítica debe repetirse en el backend. Lo veremos con Spring Validation.
Cómo incluir JavaScript en una página¶
Script interno (para demos rápidas)¶
Archivo externo (la forma recomendada)¶
Igual que con CSS, separar el código en su propio archivo mantiene el HTML legible, permite reutilizarlo en varias páginas y facilita que el navegador lo guarde en caché.
¿Dónde va la etiqueta <script>?
Al final del <body>, justo antes de cerrarlo. Si se coloca en el <head>, el script se ejecuta antes de que exista el HTML, y cualquier intento de buscar un elemento del DOM devolverá null. La alternativa moderna es usar <script src="app.js" defer></script>, que permite dejarlo en el <head> y aun así ejecutarlo cuando el documento esté listo.
Variables¶
En JavaScript existen tres formas de declarar variables:
const— valor que no se reasigna. Es la opción recomendada por defecto.let— valor que puede cambiar.var— forma antigua, con reglas de alcance confusas. Evitar su uso.
La diferencia entre var y let no es cosmética. var tiene alcance de función, lo que produce comportamientos inesperados dentro de bloques y ciclos; let y const tienen alcance de bloque, que es como funcionan la mayoría de los lenguajes y como uno espera que funcione.
if (true) {
var a = 1; // sigue existiendo fuera del if
let b = 2; // solo existe dentro del if
}
console.log(a); // 1
console.log(b); // ReferenceError
La recomendación práctica: usar const siempre, cambiar a let únicamente cuando se compruebe que el valor debe reasignarse, y no usar var.
Tipos de datos simples¶
| Tipo de dato | Representación | Ejemplo |
|---|---|---|
| Número entero | Number |
6 |
| Decimales | Float |
6.4522 |
| Cadena de caracteres | String |
"Los pipol" |
| Datos booleanos | Boolean |
true, false |
| Valor nulo | null |
null |
| No inicializado | undefined |
undefined |
| Valor que no existe después de una operación matemática | NaN |
6/0 |
Tres observaciones sobre esta tabla:
Los números no se dividen en enteros y decimales. A diferencia de Java, JavaScript tiene un único tipo Number para ambos. 6 y 6.4522 son el mismo tipo.
null y undefined no son lo mismo. undefined significa "esta variable existe pero nunca se le asignó valor"; null significa "aquí se asignó deliberadamente la ausencia de valor". El primero suele ser un descuido; el segundo, una decisión.
NaN (Not a Number) aparece cuando una operación matemática no produce un número válido. Es la fuente de errores más silenciosa del lenguaje, porque no lanza excepción: simplemente se propaga.
== frente a ===
JavaScript ofrece dos operadores de igualdad. == compara convirtiendo tipos antes, con resultados sorprendentes; === compara valor y tipo, sin conversiones.
Usar siempre ===. La comparación con conversión implícita es una de las causas más frecuentes de errores difíciles de encontrar.
Arreglos (arrays)¶
Un arreglo almacena una colección ordenada de valores. Se declara con corchetes y sus posiciones empiezan en cero.
const arreglosNumericos = [3, 4, 9, 7, 1];
const arreglosNombres = ["Pablo", "María", "Francisco", "Miguel"];
Operaciones sobre arreglos¶
| Operación | Descripción | Ejemplo |
|---|---|---|
arreglo[posicion] |
Obtiene el valor de una posición. | arreglosNumericos[0] → 3arreglosNombres[2] → "Francisco" |
arreglo[posicion] = valor |
Inserta valor en la posición indicada. |
arreglosNumericos[7] = 2 → [3,4,9,7,1, <empty>, <empty>, 2] |
arreglo.push(valor) |
Inserta valor en la última posición. |
arreglosNumericos.push(2) → [3,4,9,7,1,2] |
arreglo.length |
Devuelve la cantidad de posiciones. | arreglosNumericos.length → 5 |
arreglo.join(separador) |
Entrega un texto con todas las posiciones separadas por separador. |
arreglosNumericos.join("*") → "3*4*9*7*1" |
La segunda fila merece atención. Asignar a una posición que no existe no produce error: JavaScript agranda el arreglo y deja huecos vacíos (empty slots) en el medio. Es un comportamiento permisivo que puede pasar inadvertido y provocar resultados extraños al recorrer la colección. Para agregar elementos, push() es siempre la forma correcta.
Métodos que usaremos con frecuencia¶
Además de los anteriores, estos tres aparecen constantemente al trabajar con datos que llegan de una API:
const procesos = [
{ nombre: "Alta", activo: true },
{ nombre: "Baja", activo: false },
{ nombre: "Cambio", activo: true }
];
// map: transforma cada elemento
const nombres = procesos.map(p => p.nombre);
// ["Alta", "Baja", "Cambio"]
// filter: se queda con los que cumplen una condición
const activos = procesos.filter(p => p.activo);
// [{nombre: "Alta", ...}, {nombre: "Cambio", ...}]
// find: devuelve el primero que cumple la condición
const baja = procesos.find(p => p.nombre === "Baja");
Objetos¶
Mientras un arreglo agrupa valores por posición, un objeto los agrupa por nombre. Es la estructura con la que se representa cualquier entidad del dominio: un usuario, un proceso, una empresa.
const pipol = {
nombre: "Pablo",
apellido: "Márquez",
profesorFavorito: {
nombre: "Pedro",
apellido: "Pérez"
}
};
Formas de acceder a las propiedades¶
| Acceso | Resultado |
|---|---|
pipol.nombre |
"Pablo" |
pipol["apellido"] |
"Márquez" |
pipol.profesorFavorito["nombre"] |
"Pedro" |
pipol.profesorFavorito.apellido |
"Pérez" |
Existen dos notaciones equivalentes: la de punto (pipol.nombre) y la de corchetes (pipol["nombre"]). La de punto es más legible y es la que se usa habitualmente. La de corchetes es indispensable cuando el nombre de la propiedad está en una variable o contiene caracteres especiales:
Los objetos se anidan libremente, como muestra profesorFavorito. Esa capacidad es la que permite que un objeto represente estructuras complejas, y es exactamente la forma que tendrán los datos que el backend nos envíe en formato JSON.
Recorrer un objeto¶
// Solo las claves
for (const clave of Object.keys(pipol)) {
console.log(clave);
}
// Solo los valores
for (const valor of Object.values(pipol)) {
console.log(valor);
}
// Claves y valores a la vez
for (const [clave, valor] of Object.entries(pipol)) {
console.log(clave, "→", valor);
}
JSON: objetos que viajan por la red
JSON (JavaScript Object Notation) nació precisamente de esta sintaxis. Cuando el backend responda a una petición REST, lo hará con un texto con esta misma forma, y el frontend lo convertirá en un objeto JavaScript listo para usar. Entender los objetos ahora es entender el formato de intercambio de datos de todo el curso.
Condicionales¶
Permiten ejecutar diferentes bloques de código dependiendo de una condición.
if / else¶
const rol = "ADMIN";
if (rol === "ADMIN") {
console.log("Acceso total");
} else {
console.log("Acceso limitado");
}
Con más de dos casos se encadena con else if:
if (rol === "ADMIN") {
console.log("Acceso total");
} else if (rol === "EDITOR") {
console.log("Puede modificar");
} else {
console.log("Solo lectura");
}
switch¶
Cuando se compara una misma variable contra muchos valores, switch resulta más legible:
switch (rol) {
case "ADMIN":
console.log("Acceso total");
break;
case "EDITOR":
console.log("Puede modificar");
break;
default:
console.log("Solo lectura");
}
No olvidar el break
Sin break, la ejecución continúa hacia los casos siguientes (fall-through). Es un comportamiento heredado de C que rara vez se quiere y que produce errores difíciles de detectar.
Ciclos¶
Permiten recorrer estructuras de datos como arreglos.
for...of — recorrer los elementos¶
La forma más legible cuando solo interesan los valores:
for clásico — recorrer con índice¶
Necesario cuando hace falta saber en qué posición se está:
const procesos = ["Alta", "Baja", "Cambio"];
for (let i = 0; i < procesos.length; i++) {
console.log(i, procesos[i]);
}
while — repetir mientras se cumpla una condición¶
Cuando no se sabe de antemano cuántas iteraciones habrá:
Funciones¶
Una función agrupa un bloque de código para poder invocarlo cuantas veces sea necesario. Hay dos formas de escribirlas:
// Declaración tradicional
function saludar(nombre) {
return "Hola, " + nombre;
}
// Función flecha (arrow function)
const saludar = (nombre) => "Hola, " + nombre;
La función flecha es la sintaxis moderna y la que predomina hoy, sobre todo al pasar funciones como argumento —como vimos en map, filter y find—.
Para construir textos, las plantillas literales con backticks son más legibles que la concatenación:
Programación orientada a objetos¶
JavaScript no organiza los objetos mediante clases al estilo de Java, sino mediante prototipos: cada objeto tiene un enlace a otro objeto del que hereda propiedades y métodos. Sobre ese mecanismo, el lenguaje ofrece hoy una sintaxis de class que resulta familiar:
class Proceso {
constructor(nombre, estado) {
this.nombre = nombre;
this.estado = estado;
}
estaActivo() {
return this.estado === "ACTIVO";
}
}
const p = new Proceso("Alta de usuario", "ACTIVO");
console.log(p.estaActivo()); // true
class es azúcar sintáctico
Por debajo, JavaScript sigue trabajando con prototipos: class es una forma más cómoda de escribir lo mismo. La distinción importa poco en el uso diario, pero explica comportamientos que sorprenden a quien viene de Java.
El DOM: manipular la página desde JavaScript¶
Aquí está el verdadero poder de JavaScript en el navegador. Como vimos en la página de CSS, el navegador convierte el HTML en un árbol de objetos: el DOM.
JavaScript puede leer y modificar ese árbol en tiempo real. Cuando cambiamos un nodo del DOM, el navegador vuelve a dibujar la parte afectada de la página sin recargarla. Esa es la base de todas las aplicaciones web modernas, incluida cualquier aplicación Angular.
El trabajo con el DOM siempre sigue el mismo patrón de tres pasos:
- Seleccionar el elemento.
- Modificarlo —su contenido, sus clases o sus estilos—.
- Escuchar los eventos que produce.
Seleccionar elementos¶
| Función | Descripción | Ejemplo |
|---|---|---|
document.getElementById() |
Obtiene un elemento por su atributo id. |
document.getElementById("titulo") |
document.querySelector() |
Selecciona el primer elemento que coincida con un selector CSS. | document.querySelector(".item") |
document.querySelectorAll() |
Selecciona todos los elementos que coincidan con un selector CSS. | document.querySelectorAll("input") |
querySelector acepta exactamente la misma sintaxis de selectores que CSS: .clase, #id, div > p, input[type="text"]. Todo lo aprendido en la página anterior se reutiliza aquí sin cambios.
Modificar contenido y estilos¶
| Función / Propiedad | Descripción | Ejemplo |
|---|---|---|
element.textContent |
Obtiene o modifica el texto interno del elemento. | titulo.textContent = "Nuevo texto" |
element.innerHTML |
Obtiene o modifica el contenido HTML del elemento. | div.innerHTML = "<p>Hola</p>" |
element.style |
Modifica estilos CSS directamente sobre el elemento. | div.style.color = "red" |
element.classList.add() |
Agrega una clase CSS. | div.classList.add("activo") |
element.classList.remove() |
Elimina una clase CSS. | div.classList.remove("activo") |
element.classList.toggle() |
Agrega o quita una clase según su estado actual. | div.classList.toggle("visible") |
textContent frente a innerHTML
innerHTML interpreta lo que se le asigna como HTML. Si ese contenido proviene de algo que escribió un usuario, se abre la puerta a inyección de código en la página (XSS). La regla es: usar textContent para insertar texto y reservar innerHTML para contenido que el propio código genera y controla.
Preferir clases sobre estilos en línea
Modificar element.style funciona, pero dispersa la presentación entre el CSS y el JavaScript. Es más limpio definir el estilo en la hoja de estilos y limitarse a agregar o quitar clases con classList. Así CSS sigue siendo el único responsable de cómo se ve la página.
Trabajar con atributos¶
| Función | Descripción | Ejemplo |
|---|---|---|
element.setAttribute() |
Asigna un atributo al elemento. | input.setAttribute("disabled", true) |
element.getAttribute() |
Obtiene el valor de un atributo. | img.getAttribute("src") |
element.removeAttribute() |
Elimina un atributo del elemento. | input.removeAttribute("disabled") |
Crear y eliminar elementos¶
| Función | Descripción | Ejemplo |
|---|---|---|
document.createElement() |
Crea un nuevo elemento HTML. | document.createElement("div") |
element.appendChild() |
Agrega un elemento hijo. | lista.appendChild(item) |
element.remove() |
Elimina un elemento del DOM. | div.remove() |
const lista = document.querySelector("#procesos");
const item = document.createElement("li");
item.textContent = "Nuevo proceso";
lista.appendChild(item);
Eventos¶
Un evento es algo que ocurre en la página y a lo que el código puede reaccionar: un clic, una tecla, el envío de un formulario, el paso del mouse. Se escuchan con addEventListener(), que recibe el nombre del evento y la función que debe ejecutarse.
Partamos de este HTML:
Y este JavaScript:
const boton = document.getElementById("btn-saludar");
const mensaje = document.getElementById("mensaje");
boton.addEventListener("click", () => {
mensaje.textContent = "¡Hola! Hiciste clic.";
mensaje.classList.add("visible");
});
Ahí está el ciclo completo: se selecciona un elemento, se escucha un evento y, cuando ocurre, se modifica el DOM. La página cambia sin recargarse y sin ninguna petición al servidor.
Eventos más frecuentes¶
| Evento | Se dispara cuando… |
|---|---|
click |
Se hace clic sobre el elemento. |
submit |
Se envía un formulario. |
input |
Cambia el valor de un campo, en cada pulsación. |
change |
El campo pierde el foco después de haber cambiado. |
keydown / keyup |
Se presiona o se suelta una tecla. |
DOMContentLoaded |
El documento HTML terminó de cargarse. |
Validar un formulario¶
Este es el caso de uso que aparece en el taller de la Wiki y que reúne todo lo visto:
<form id="contacto">
<label for="correo">Correo electrónico</label>
<input type="text" id="correo">
<span id="error-correo" class="error"></span>
<button type="submit">Enviar</button>
</form>
const formulario = document.getElementById("contacto");
const correo = document.getElementById("correo");
const error = document.getElementById("error-correo");
formulario.addEventListener("submit", (evento) => {
evento.preventDefault(); // evita que el formulario se envíe
const valor = correo.value.trim();
if (valor === "") {
error.textContent = "El correo es obligatorio.";
return;
}
if (!valor.includes("@") || !valor.includes(".")) {
error.textContent = "El formato del correo no es válido.";
return;
}
error.textContent = "";
console.log("Formulario válido, listo para enviar.");
});
evento.preventDefault()
Muchos eventos tienen un comportamiento por defecto del navegador: un formulario se envía y recarga la página, un enlace navega a otra dirección. preventDefault() cancela ese comportamiento para que el código decida qué hacer. En una validación es imprescindible: sin él, la página se recarga antes de que el mensaje de error alcance a verse.
Actividad 3 · JavaScript interactivo¶
Añade comportamiento a la misma página.
Objetivo
Usar el DOM y los eventos para crear una interacción útil y visible.
Pasos
- Crea
app.jsy enlázalo desdeindex.html. - Selecciona elementos del DOM mediante
ido clase. - Escucha un evento de clic o envío de formulario.
- Cambia contenido, clases o estilos y muestra retroalimentación al usuario.
Entregable
index.html + estilos.css + app.js + evidencia breve de la interacción.
Verificación
✓ La interacción responde sin recargar la página. ✓ El DOM cambia de forma visible y coherente. ✓ No aparecen errores en la consola. ✓ HTML, CSS y JavaScript permanecen separados y ordenados.
Cierre del módulo¶
Con las tres actividades completas, la página reúne las tres capas trabajando juntas:
| Capa | Archivo | Responsabilidad |
|---|---|---|
| HTML | index.html |
Qué información hay y cómo está estructurada |
| CSS | estilos.css |
Cómo se ve, en escritorio y en móvil |
| JavaScript | app.js |
Cómo responde a lo que hace el usuario |
Que sean tres archivos separados no es un requisito administrativo: es la primera aplicación concreta del principio que recorre todo el curso —separar responsabilidades reduce el impacto del cambio—. Rediseñar la página no obliga a tocar la lógica; cambiar la validación no obliga a tocar el contenido.
A partir de aquí, el siguiente paso es dejar de escribir el HTML a mano y generarlo desde el servidor según los datos de la aplicación. De eso se ocupa el módulo de Thymeleaf.