Saltar a contenido

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)

<script>
  console.log("Hola desde JS");
</script>

Archivo externo (la forma recomendada)

<script src="app.js"></script>

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.
const nombre = "Pablo";

let edad = 20;
edad = 21;

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.

let a;              // undefined
let b = null;       // null, asignado a propósito

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.

const resultado = "hola" * 2;   // NaN
console.log(resultado + 5);     // NaN, el error se arrastra

== frente a ===

JavaScript ofrece dos operadores de igualdad. == compara convirtiendo tipos antes, con resultados sorprendentes; === compara valor y tipo, sin conversiones.

"5" == 5     // true  ← convierte el texto a número
"5" === 5    // false ← tipos distintos

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]3
arreglosNombres[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.length5
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:

const campo = "apellido";
console.log(pipol[campo]);   // "Márquez" — con punto sería imposible

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:

const procesos = ["Alta", "Baja", "Cambio"];

for (const p of procesos) {
  console.log(p);
}

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á:

let intentos = 0;

while (intentos < 3) {
  console.log("Intento", intentos);
  intentos++;
}

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:

const nombre = "Pablo";
console.log(`Hola, ${nombre}. Bienvenido al curso.`);

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:

  1. Seleccionar el elemento.
  2. Modificarlo —su contenido, sus clases o sus estilos—.
  3. 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:

<button id="btn-saludar">Saludar</button>
<p id="mensaje"></p>

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

  1. Crea app.js y enlázalo desde index.html.
  2. Selecciona elementos del DOM mediante id o clase.
  3. Escucha un evento de clic o envío de formulario.
  4. 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.