Manipulación de documentos web

Introducción

La web se construyó a partir del lenguaje HTML. En sus principios, este estaba muy limitado, tanto en etiquetas de estructura como de estilo en un contexto que ahora conocemos como Web 1.0. Se trataban de simples "expositores" de contenido: documentos estáticos y lineales donde el visitante adoptaba un rol de consumidor pasivo.

La interactividad era inexistente, y la única vía para actualizar el contenido era que el webmaster modificara manualmente el código fuente del archivo HTML y lo subiera de nuevo al servidor. Por otro lado, el navegador era un visor de documentos fijos, incapaz de reaccionar a las acciones del usuario sin recargar por completo la página web.

Con la Web 2.0 llegó la web dinámica. Las páginas webs pasan de ser expositores de contenido a aplicaciones con las que un usuario puede interactuar mediante el navegador y a través de la web. Se convierten en aplicaciones web. La primera aproximación se hizo desde el punto de vista del servidor mediante una técnica llamada SSR (Server Side Rendering), que consiste en lo siguiente:

Esta primera aproximación, sin embargo, tiene varios inconvenientes, entre los que destacan:

El enfoque a día de hoy consiste en una carga inicial del sitio web generado desde el servidor, que envía todos los recursos que el navegador del usuario necesita para encargarse de la representación del sitio web y el manejo de las interacciones. Algunas de estas interacciones requerirán que el servidor también realice otras acciones (como registrar un usuario o iniciar una sesión) y proporcione datos bajo demanda, pero sin necesidad de generar y enviar un documento HTML nuevo cada vez.

A diferencia del modelo SSR tradicional, una vez el navegador recibe este "esqueleto" inicial, hará uso de un lenguaje de programación de cliente (JavaScript - JS) para tomar el control total de la interfaz.

A partir de este momento, el navegador ya no ve el HTML como un simple archivo de texto, sino como un árbol de nodos llamado DOM (Document Object Model). Esta es la pieza clave de la web moderna, ya que resuelve los problemas recién mencionados:

A día de hoy, JS se ha vuelto un lenguaje de programación muy potente que, sumado a la mejora del hardware y de la capacidad de cómputo de los ordenadores actuales, ha evolucionado hasta convertir el navegador en un entorno capaz de procesar tareas de alta complejidad, incluyendo la ejecución de videojuegos y aplicaciones de renderizado 3D en tiempo real.

En esta Unidad de Trabajo empezaremos poco a poco, aprendiendo cómo modificar el DOM de un sitio web mediante JS para hacer que nuestras webs sean dinámicas desde el cliente (más adelante veréis también cómo hacerlo desde el servidor). Es el uso principal y más importante para un desarrollador web. Sin embargo, también intentaremos desarrollar algún juego.

Para ejecutar tu primer código JS, abre un navegador web, herramientas para desarrolladores, pestaña de consola y escribe la siguiente línea:

alert('¡Hola, Mundo!')

Se mostrará el siguiente popup:

image-20260319130829636

Nota: según el navegador que utilices, las herramientas de desarrollador serán accesibles de diferentes maneras. En navegadores con motor Chromium, se utiliza típicamente el atajo Ctrl + Shift + I

1. Características de JavaScript

Se trata de un lenguaje de alto nivel, interpretado, de propósito general, multiparadigma con tipado dinámico:

Muy importante: esta comparación se ha realizado usando Java como referencia debido a que es el lenguaje que ya conocéis y domináis. Sin embargo, es vital recordar que tienen muy poco que ver entre sí. Sus arquitecturas, propósitos y filosofías de diseño son radicalmente distintas, y su parecido se limita, casi exclusivamente, a las cuatro primeras letras de su nombre. Java es a JavaScript lo que pollo es a repollo. El origen del nombre de JS es puramente marketing debido al éxito que tuvo (y tiene) Java.

2. Primeros pasos

Para empezar a programar en JS tenemos varias opciones:

La opción de fichero externo es el estándar actual, y es lo que usaremos a partir de ahora. Al igual que con los ficheros CSS separamos el estilo de la estructura, con los ficheros JS separamos el código JavaScript del HTML. Es más, en el HTML no deberíamos tener ni siquiera los atributos que referencian a las funciones definidas, tal y como se ha comentado en la opción atributo, sino que deberemos añadirlo con selectores dentro del fichero JS. Siguiendo el ejemplo anterior, tendríamos lo siguiente en el fichero HTML:

<button id="procesarDatosButton">Enviar formulario</button>

Y lo siguiente en el fichero JS:

const boton = document.getElementById('procesarDatosButton');

function procesarDatos() {
    alert('¡Formulario enviado con éxito!');
}

boton.addEventListener('click', procesarDatos);

Pero, para entender mejor todo esto, primero debemos aprender sobre la sintaxis de JS, el DOM y los selectores.

Importante, antes de seguir: vamos a trabajar con ficheros, pero querremos mostrar información por consola. Para ello, debemos acostumbrarnos a utilizar console.log

3. Sintaxis de JavaScript

La sintaxis de JS es parecida a la de Java, y esta es parecida (a su vez) a la de C en muchos aspectos. Con esto, lo que se quiere decir es que en la mayoría de lenguajes de programación con base principalmente procedimental e imperativa existe una sintaxis similar. Lo que se pretende en este apartado es obviar los aspectos básicos de la programación (incluyendo la sintaxis), que son compartidos entre la mayoría de esos lenguajes, para hacer un énfasis en las particularidades de JS y sus diferencias con Java (que es el lenguaje que habéis aprendido)

3.1 Variables y tipos de datos

JS es un lenguaje con tipado débil y dinámico:

3.1.1 Declaración de variables: let y const

Existen 3 maneras de declarar variables en JavaScript: let, const y var, pero var es una manera antigua y, aunque a día de hoy tiene un uso muy específico, en la inmensa mayoría de casos utilizaremos solamente let y const

let contenedor; 			// Sin problema
contenedor = 42; 			// Todo gucci
contenedor = 'vamos bien';	// Tú lo has dicho

const edad = 33;			// No puedes cumplir más años
const nombre;				// Error de inicialización
edad = edad + 1;			// Error de asignación

3.1.2 Tipos de datos

Existen 7 tipos primitivos y los objetos. Los objetos los veremos más adelante:

3.1.2.1 null vs undefined

Viniendo desde Java, esta distinción puede ser confusa. Se resume a:

Por ejemplo, supón que queremos hacer una búsqueda en BBDD de un usuario, pero el usuario puede que no exista:

const usuarioId = 42;
let usuario; // Declaramos la variable sin asignarle un valor

if (!buscarUsuario(idUsuario)) { // Si no encuentra ningún usuario
    usuario = null; // Le damos a la variable el valor null de manera explícita
}
3.1.2.2 Operador typeof

En Java se utiliza instanceof para comprobar el tipo de un objeto. En JS se utiliza typeof

console.log(typeof 42);
console.log(typeof 42.0);
console.log(typeof '42');
console.log(typeof true);
console.log(typeof undefined);
console.log(typeof null); // Cuidado aquí
console.log(typeof 1n);
console.log(typeof Symbol('id'));

let nombre;
typeof(nombre)

JS está repleto de bugs reconocidos que no se han arreglado hasta ahora por la compatibilidad hacia el pasado. Uno de estos bugs es el typeof null. Pese a que null es un tipo primitivo, la respuesta de typeof es object

3.1.3 Conversión de tipos

La conversión es diferente a la coerción. La conversión se realiza de manera explícita utilizando uno de los siguientes constructores

3.1.3.1 Number()

Recibe un valor y devuelve un número

Number('42'); // Devuelve 42
Number('a'); // Devuelve NaN
Number(42n); // Devuelve 42
Number(true); // Devuelve 1
Number(false); // Devuelve 0
Number(undefined); // Devuelve NaN
Number(null); // Devuelve 0
3.1.3.2 String()

Recibe un valor y devuelve una cadena

String(42); // Devuelve '42'
String(42n); // Devuelve '42'
String(true); // Devuelve 'true'
String(false); // Devuelve 'false'
String(undefined); // Devuelve 'undefined'
String(null); // Devuelve 'null'

¿Qué devolverá Boolean(String('false'))?

3.1.3.3 Boolean()

Recibe un valor y devuelve un booleano

Boolean(42); // Devuelve true
Boolean(0); // Devuelve false
...

Sólo hay 6 valores considerados false (también llamados falsy):

3.1.3.4 BigInt()

Recibe un valor y devuelve un entero grande. Cuidado: en muchos casos puede provocar error:

BigInt(42); // Devuelve 42n
BigInt('42'); // Devuelve 42n
BigInt(''); // Devuelve 0n
BigInt(true); // Devuelve 1n
BigInt(false); // Devuelve 0n
BigInt('a'); // ERROR
BigInt(undefined); // ERROR
BigInt(null); // ERROR
BigInt(NaN); // ERROR
3.1.3.5 Symbol()

Recibe un valor y devuelve un símbolo. Admite cualquier valor

3.2 Operadores y comparadores

En JS, los operadores aritméticos (+, -, *, /, %, ++, --) funcionan exactamente igual que en Java. Además, JS cuenta con el operador de exponenciación (**), que es equivalente a Math.pow() de Java

2**10; // Idéntico a Math.pow(2, 10), tanto en Java como en JS

De la misma manera, los operadores lógicos AND, OR y NOT son idénticos a los de Java (&&, || y !)

Sin embargo, en la comparación de valores es donde debemos tener especial cuidado

3.2.1 Igualdad estricta VS Igualdad débil

A diferencia de Java, JS permite comparar "cualquier cosa" con "cualquier cosa" sin que esto provoque conflictos de tipos. Esto puede ser útil en aquellos casos en los que en otros lenguajes de programación necesitaríamos hacer un cambio de tipo explícito. Sin embargo, también habrá casos en los que queramos comprobar que 2 valores sean exactamente iguales (en valor y tipo de dato). Para gestionar esto, JS proporciona 2 comparadores de igualdad:

  1. Igualdad débil (==): antes de comparar, intenta convertir ambos valores a un tipo común (coerción)

    '5' == 5; // true
    0 == false; // true
    '' == 0; // true
    null == undefined; // true
    
  2. Igualdad estricta (===): tanto valor como tipo deben ser idénticos

    '5' === 5; // false
    0 === false; // false
    '' === 0; // false
    null === undefined; // false
    

Por lo general, es recomendable usar siempre la igualdad estricta (===) a menos que hay un motivo muy específico en el que la igualdad débil nos sea útil. Por ejemplo:

let usuario;

// Comprobamos que usuario sea null o undefined con igualdad estricta
usuario === null || usuario === undefined;

// Su equivalente con igualdad débil
usuario == null;

3.3 Control de flujo

En JS, las estructuras de if, else if, else, switch, while, do-while, for y try-catch tienen una sintaxis idéntica a Java. Sin embargo, conviene destacar algunas particularidades

Las sentencias de salto break y continue también funcionan exactamente igual

3.3.1 Evaluación de condiciones (Truthy y Falsy)

Si bien en Java las evaluaciones de condiciones sólo aceptan expresiones que devuelvan valores boolean, en JS se puede evaluar "cualquier cosa". Hay una serie de valores que se consideran falsos. Todos los demás se consideran verdaderos. Esto se ha explicado ya aquí. Ejemplo:

if (nombre) {
    console.log(`Hola, ${nombre}`);
}

/*
    En Java sería algo así:
    if (nombre != null && !nombre.isEmpty() {
        ...
    }
*/

3.3.2 Iteración sobre colecciones

En Java, una forma muy cómoda de iterar las colecciones es con el bucle for-each. En JS existen 2 variantes pero que tienen propósitos radicalmente distintos

3.3.2.1 for-of: valores

Es el equivalente directo al for (Tipo elemento : coleccion) de Java. Se utiliza para recorrer valores

const lenguajes = ['Java', 'JS', 'PHP'];

for (const lenguaje of lenguajes) {
    console.log(lenguaje);
}

/*
	En Lava sería algo así:
	for (String lenguaje : lenguajes) {
		...
	}
*/
3.3.2.2 for-in: claves

Este bucle no recorre valores, sino las propiedades de un objeto. Aún no se han introducido los objetos, pero en JS son mucho más simples que en otros lenguajes. Para buscar un equivalente en Java ahora mismo, podemos decir que podrían ser algo así como "mapas clave-valor"

// Esto es un objeto en JS
const persona = {
    nombre: 'Ignacio',
    edad: 33
}

for (const propiedad in persona) {
    console.log(propiedad); // Imprime 'nombre", 'edad'
    console.log(persona[propiedad]); // Imprime 'Ignacio', 33
}

/*
	En Java sería algo así:
	map.forEach((k, v) -> {
		...
	});
*/

3.3.3 Declaración de variables en bucles for y for-of/for-in

Como se ha mencionado aquí, la elección entre let y const a la hora de declarar una variable depende de si el valor va a ser reasignado. Esto es de especial importancia en los bucles:

3.4 Funciones

Viniendo de Java, aquí es donde JS empieza a parecer algo más diferente. En Java todo debe vivir dentro de una clase y, por tanto, todas las funciones son, en realidad, métodos. En JS, las funciones son "ciudadanos de primera clase" (en serio, se les llama así). Esto quiere decir que no dependen de clases para existir y son tratadas como cualquier otro dato: pueden guardarse en variables, pasarse como argumentos a otras funciones o ser devueltas por estas. Esto puede parecer "caótico" al principio, pero permite aplicar patrones de programación funcional muy potentes. No te preocupes, no llegaremos a tanto, este tema es más introductorio

3.4.1 Funciones como declaración

Es la forma más tradicional y la que más recuerda a los métodos de Java. La firma de la función consta de:

El cuerpo de la función va entre llaves

function saludar(nombre) {
    return `Hola, ${nombre}`;
}

console.log(saludar('Ignacio')); // 'Hola, Ignacio'

Hoisting: las funciones declaradas se "elevan" al principio del código, de manera que puedes llamarlas en una línea anterior a aquella en la que la defines. Puedes definir la función en la línea 100 y llamarla en la 1. A pesar de ello, es muy recomendable seguir un orden en los lenguajes de scripting

3.4.2 Funciones como expresión

Aquí es donde empezamos a ver que las funciones, en realidad, también son "datos". Podemos crear una función y asignarla directamente a una variable

const saludar = function(nombre) {
    return `Hola, ${nombre}`;
}

console.log(saludar('Ignacio')); // 'Hola, Ignacio'

A diferencia de las anteriores, estas no tienen hoisting. No puedes usarlas antes de que el intérprete llegue a esa línea de código, de la misma manera que no puedes acceder al valor de una variable que aún no has declarado

3.4.3 Funciones flecha

Son el equivalente a las expresiones lambda de Java. Son más compactas y se han convertido en el estándar de JS moderno

const saludar = nombre => {
    return `Hola, ${nombre}`;
}

// Si sólo tiene una línea, el "return" y las llaves son opcionales
const saludar = nombre => `Hola, ${nombre}`;

console.log(saludar('Ignacio')); // 'Hola, Ignacio'

3.4.4 Particularidades

Además de lo ya mencionado, conviene resaltar otras particularidades:

3.5 Arrays

Los arrays se pueden crear de 2 maneras diferentes:

const misMaterias = ['DWES', 'LMSGI', 'Digitalización', 'Fundamentos'];
const misAmigos = new Array('Don Pepito', 'Don José');

La manera de acceder a sus elementos es mediante índices usando el nombre del array y corchetes [], empezando siempre desde el índice 0 y hasta el N-1, siendo N el total de elementos

console.log(misMaterias[1]); // 'LMSGI'

Podemos obtener el total de elementos mediante el nombre del array seguido de .length

console.log(misMaterias.length)

Apreciarás que se trata de una propiedad, no de una función (no lleva paréntesis). Esto confirma que los arrays en JS son, en realidad, objetos. Como mencionamos en el apartado de Tipos de datos, JS sólo distingue entre 7 tipos de datos primitivos y objetos. Podemos comprobar el tipo de dato de un array con typeof

typeof misMaterias; // 'object'

Si necesitamos saber con certeza si algo es un array, en JS podemos utilizar un método específico:

Array.isArray(misMaterias); // true

3.5.1 Características fundamentales

En JS, los arrays son dinámicos, heterogéneos y multidimensionales:

const cosas = [42, 'Hola', ["otro array"], false];
console.log(cosas.length); // 4
console.log(cosas[2][0]); // 'otro array'

3.5.2 Funciones de arrays

A diferencia de los arrays estáticos de Java, JS proporciona una interfaz que permite su uso como pila (stack) o como cola (queue)

3.5.2.1 Métodos de inserción y extracción

Estos métodos alteran la estructura del array original

const misMaterias = ['DWES', 'LMSGI'];

misMaterias.push('DIGI', 'FUND'); // Devuelve 4. Nuevo array: ['DWES', 'LMSGI', 'DIGI', 'FUND']
misMaterias.unshift('PROY'); // Devuelve 5. Nuevo array: ['PROY', 'DWES', 'LMSGI', 'DIGI', 'FUND']

const ultimo = misMaterias.pop(); // Devuelve 'FUND'
const primero = misMaterias.shift(); // Devuelve 'PROY'
3.5.2.2 Métodos de consulta

Permiten localizar elementos o verificar su existencia dentro de la colección

const misMaterias = ['DWES', 'LMSGI', 'DIGI'];

const lmsgi = misMaterias.indexOf('LMSGI'); // 1
const proy = misMaterias.indexOf('PROY'); // -1

if (misMaterias.includes('DWES')) {
    console.log('Aprende Laravel, lo agradecerás en el futuro')
}

3.6 Objetos

En Java, todo objeto es instancia de una clase previamente definida. En JS los objetos tienen una naturaleza distinta: son colecciones dinámicas de pares clave-valor. Lo más parecido en Java sería, quizás, los Map. Sin embargo, puesto que las funciones en JS son "ciudadanos de primer orden", estas colecciones permiten almacenar también métodos por lo que, a efectos prácticos, se comportan como objetos convencionales, pero con una flexibilidad mucho mayor

3.6.1 Definición de objetos

Un objeto se define utilizando llaves {}. Las claves (propiedades) se separan de sus valores mediante dos puntos :, y cada par se separa por comas

const profe = {
    nombre: 'Ignacio',
    nacimiento: 1492,
    materias: ['DWES', 'LMSGI', 'Fundamentos', 'DigiOfi', 'PROY']
}

3.6.2 Acceso a miembros

Existen 2 mecanismos para acceder a la información almacenada:

  1. Notación punto (.): la más común y similar a Java

    console.log(profe.nombre); // 'Ignacio'
    
  2. Notación corchetes ([]): parecida al acceso por índice a un array. Es útil cuando no sabemos de antemano el miembro que queremos consultar

    // La función "prompt" nos permite solicitar datos en un popup
    const consulta = prompt('¿Qué quieres saber del profe? (nombre, edad, materias)');
    console.log(profe[consulta]);
    

Para acceder a los miembros en métodos dentro del objeto, haremos uso de la palabra clave this, igual que en Java

const profe = {
    ...
    saludar: function() {
        return `Hola, mi nombre es ${this.nombre}`;
    }
}

Para crear métodos dentro de un objeto es muy importante no utilizar funciones flecha, ya que estas no tienen acceso al contexto del objeto que las contiene, sino al del ámbito en el que fueron creadas. A esto se le llama lexical scoping. Es algo bastante avanzado que no vamos a utilizar

3.6.3 Mutabilidad

Es posible añadir, modificar o eliminar propiedades y métodos en tiempo de ejecución

profe.apellido = 'Mascarell'; // Extensión
profe['apellido'] = 'Mascarell'; // Equivalente con corchetes
profe.nacimiento = 1992; // Modificación
delete profe.apellido; // Eliminación

Nota: a pesar de que declaremos una variable que contiene un objeto con const, sí podemos modificar el objeto en sí, pero no reasignar la variable. Protegemos la referencia. Es decir, no podemos hacer profe = {}

Por supuesto, también podemos añadir métodos

profe.calcularEdad = function() {
    return 2026 - this.nacimiento;
}

profe.calcularEdad(); // 34

4. JavaScript Object Notation (JSON)

Como hemos visto, los objetos en JS son muy sencillos, a la vez que flexibles. Esta sencillez caló tanto en la industria que se decidió extraer su sintaxis para crear un estándar de intercambio de datos ligero y universal: JSON. Aunque las siglas signifiquen JavaScript Object Notation, a día de hoy es un formato independiente del lenguaje. Es el estándar de facto en APIs modernas, como veréis en DWES y DWEC (Desarrollo Web en Entorno Servidor y Desarrollo Web en Entorno Cliente)

La sintaxis más simple de JSON permite la creación de documentos mucho más ligeros que XML y, además, son muy sencillos de procesar desde JS y otros lenguajes de programación. Sin embargo, no todo son ventajas, cada lenguaje de marcado tiene su utilidad

Característica XML JSON
Legibilidad Verbose, muchas etiquetas de apertura y cierre Compacto y directo
Tamaño del fichero Mayor (las etiquetas repiten mucho texto) Menor (ideal para intercambio de datos)
Tipado Todo es texto (requiere esquema/DTD) Soporta números, booleanos y arrays de forma nativa
Procesamiento (web) Requiere DOMParser, complejo Se convierte en objeto JS rápidamente
Uso principal Documentos complejos, configuración, intercambio de datos semánticos APIs, servicios web y aplicaciones en tiempo real

En los 2 siguientes bloques de código se muestra una comparativa entre XML y JSON

<?xml version="1.0" encoding="UTF-8" ?>
<consolas>
    <consola>
        <nombre>XBox Series</nombre>
        <fabricante>Microsoft</fabricante>
        <anyo>2020</anyo>
    </consola>
    <consola>
        <nombre>Play Station 5</nombre>
        <fabricante>Sony</fabricante>
        <anyo>2020</anyo>
    </consola>
</consolas>
{
    "consolas": [
        {
            "nombre": "XBox Series",
            "fabricante": "Microsoft",
            "anyo": 2020
        },
        {
            "nombre": "Play Station 5",
            "fabricante": "Sony",
            "anyo": 2020
        }
    ]
}

4.1 Sintaxis

Aunque JSON derive de JS, no podemos escribirlo de la misma manera. Es un formato de texto plano con reglas sintácticas innegociables:

  1. Comillas dobles siempre: tanto las claves/propiedades (parte "izquierda" de los :) como los valores (parte "derecha" de los :) de texto deben usar comillas dobles "". Las comillas simples o los acentos abiertos darán error
  2. Sólo datos: no se permiten métodos, sólo datos

4.2 Usos de JSON

4.2.1 Envío de datos

En la web moderna, el intercambio de datos entre cliente (frontend) y servidor (backend) se realiza de forma constante, sin necesidad de recargar la página. Para realizar el intercambio de datos, el estándar de facto a día de hoy es JSON. Ante cualquier interacción, sucede lo siguiente:

  1. Petición: el navegador solicita o envía datos al servidor
  2. Serialización: para que los datos viajen por Internet, los objetos que usamos en los programas deben convertirse a cadena de texto (JSON)
  3. Deserialización: al recibir la cadena de texto, el receptor convierte de nuevo el texto en objeto para poder trabajar con sus propiedades
4.1.1.1 JSON.stringify() : de objeto a texto - serialización

Utilizado para enviar datos al servidor en formato de texto

const peliculaJS = {
    titulo: 'El Resplandor',
    director: 'Stanley Kubrick'
};
const peliculaJSON = JSON.stringify(peliculaJS);
console.log(peliculaJSON); // '{"titulo":"El Resplandor","director":"Stanley Kubrick"}'
4.2.1.2 JSON.parse(): de texto a objeto - deserialización

Utilizado para convertir una cadena de caracteres en un objeto manejable

const peliculaJSON = `{
    "titulo": "El Resplandor",
    "director": "Stanley Kubrick"
}`;
const peliculaJS = JSON.parse(cadenaJSON);
console.log(peliculaJS.titulo); // "El Resplandor"
console.log(peliculaJS.director); // Stanley Kubrick

4.2.2 Otros usos

4.2.2.1 Bases de datos NoSQL

En auge de JSON ha dado lugar a las Bases de Datos NoSQL Orientadas a Documentos:

4.2.2.2 Configuración de entornos y herramientas

Casi todas las herramientas que usan los desarrolladores (VS Code, npm, Docker...) han sustituido antiguos ficheros de configuración .xml o .ini por .json

5. El DOM (Document Object Model)

Es una pieza clave de la web moderna. Gracias al DOM, el navegador deja de ver una web como un simple archivo de texto en formato HTML y lo transforma en un árbol de objetos vivos en memoria. Esto permite que JS tome el control total de:

Es muy similar a la representación arborescente de un XML, que también tiene su propio DOM

El DOM nos proporciona una API (Interfaz de Programación de Aplicaciones) que nos permite acceder a distintas partes del documento. Esta API es independiente de JS. Es decir, podemos acceder a ella no solamente desde JS, sino también desde otros lenguajes de programación. Sin embargo, JS es el lenguaje que todos los navegadores ejecutan de forma nativa, convirtiéndose en la herramienta principal para la manipulación del DOM

En JS, todos los métodos del DOM se ejecutan a través de un objeto global llamado document

Puedes consultar más sobre ello aquí: https://developer.mozilla.org/es/docs/Web/API/Document

5.1 Tipos de Nodos

No todo en el árbol del DOM es una etiqueta en HTML. El navegador descompone el HTML en diferentes tipos de nodos:

5.2 Selectores

Para poder cambiar un estilo, leer un valor, modificar un texto... Primero debemos "apuntar" al elemento correcto. Para ello, la API del DOM nos ofrece varios métodos de búsqueda. A continuación se muestra un fragmento de código HTML sencillo para ejemplificar los métodos de selección:

01 <p id="parrafo1" class="ejemplo">Párrafo 1</p>
02 <p>Párrafo 2</p>
03 <p>Párrafo 3</p>
04 <p class="ejemplo">Párrafo 4</p>
05 <h1>Título 1</h1>
06 <h1>Título 2</h1>
Método Descripción Ejemplo Resultado
getElementById Obtiene 1 elemento, aquel cuyo atributo id coincide con el parámetro document.getElementById('parrafo1') 01
getElementsByClassName Obtiene N elementos, aquellos cuyo atributo class coincida con el parámetro document.getElementsByClassName('ejemplo') 01 y 04
querySelector Obtiene 1 elemento, el primero que encaje con el selector CSS indicado document.querySelector('h1') 05
querySelectorAll Obtiene N elementos, todos los que encajen con el selector CSS indicado document.querySelectorAll('p:not(.ejemplo)') 02 y 03

Aunque querySelector y querySelectorAll son más versátiles y modernos, es habitual utilizar getElementById y getElementsByClassName por motivos de rendimiento, ya que estos últimos son más directos en la búsqueda

5.3 Acceso y modificación del contenido

Una vez que hemos seleccionado un elemento, el siguiente paso es interactuar con lo que contiene, y aquí diferenciamos principalmente dos propiedades: textContent y value

5.3.1 textContent

Nos permite acceder o modificar el texto que hay dentro de un elemento

const parrafo = document.getElementById('parrafo1');

console.log(parrafo.textContent); // "Párrafo 1"
parrafo.textContent = 'Nuevo párrafo';

5.3.2 value

A diferencia de elementos como <p> o <h1>, los de entrada de datos (formularios) no guardan su información en textContent , sino en un atributo llamado value. Usaremos value con:

const inputNombre = document.querySelector('#userName');

console.log(inputNombre.value); // Lo que ha escrito el usuario
input.Nombre.value = ''; // Para limpiar/vaciar el campo

5.4 Manipulación de atributos y clases

Además del contenido, el dinamismo de una web también incluye la alteración de sus atributos (como el src de una imagen) o sus estilos (bien de manera directa o bien mediante clases de CSS)

5.4.1 Atributos directos

Podemos acceder a cualquier atributo directamente como si fuera una propiedad del objeto:

const imagen = document.querySelector('img');
imagen.src = 'img/ies-albares.png';
imagen.alt = 'Logo del IES Los Albares';

const jugador1 = document.querySelector('#jugador-1')
jugador1.style = 'background-color: green';

5.4.2 El objeto classList

Para gestionar las clases, en lugar sobreescribir toda la cadena de la clase, podemos usar classList, que ofrece métodos mucho más limpios:

const parrafo = document.getElementById('parrafo1');
parrafo.classList.add('importante');

6. Eventos

Un evento es una notificación asíncrona de que el estado de un objeto ha cambiado o que se ha producido una interacción específica. Por ejemplo: un usuario que hace click en un elemento de la interfaz gráfica

Hasta ahora, cuando hemos pedido datos al usuario usando prompt() (o Scanner en Java), ha sucedido lo siguiente:

Las interacciones asíncronas se diferencian de estas en que el programa se va a ejecutar sin esperar al usuario, pero va a estar atento, "a la escucha", a cuando el usuario interaccione con elementos de la interfaz. Se produce una inversión de control

6.1 Registro de eventos: addEventListener

Para implementar la "escucha", el estándar actual de JS utiliza el método addEventListener. La sintaxis es la siguiente:

elemento.addEventListener('tipo_de_evento', funcion_a_ejecutar);

Antes se utilizaban atributos de HTML, como onclick pero, como ya se ha mencionado antes, es preferible separar completamente la estructura del comportamiento. Además, el enfoque actual permite añadir múltiples funciones al mismo evento

6.1.1 Eventos globales

Por lo general, registraremos los eventos sobre elementos específicos, como un botón o un campo de texto, pero existen interacciones que deben escucharse a nivel de toda la página. El caso típico es cuando el usuario pulsa una tecla como "Escape" para interrumpir una acción o para cerrar un diálogo o un menú emergente

En estos casos, el receptor habitual del evento es el objeto document

document.addEventListener('keydown', (e) => {
    if (e.key === "Escape") {
        console.log("Cerrando modal...");
    }
});

6.2 Anatomía de un evento. El objeto event

Cuando se produce una interacción, ya sea de origen físico (hardware) o lógico (como un temporizador o la carga de un recurso), el navegador genera automáticamente un objeto de evento. Este objeto se pasa (o no, lo decidimos nosotros) a la función encargada de manejarlo (también llamada función de callback)

elemento.addEventListener('click', (event) => {
    console.log(event);
});

El objeto event (también suele usarse el nombre e) es una instancia de Event, y algunas de sus propiedades y métodos más importantes son:

6.3 Tipos de eventos más frecuentes

Hay cientos de eventos, puedes consultar todos los tipos en la especificación oficial de W3C sobre eventos del DOM, aunque es más cómodo hacerlo aquí

6.3.1 Eventos de ratón

6.3.2 Eventos de teclado

6.4 El caso especial de los temporizadores

A diferencia de los eventos mencionados en el apartado anterior, los temporizadores no se registran sobre un elemento del DOM, sino que utilizamos funciones globales del navegador para gestionar el tiempo. A pesar de que definamos explícitamente cuándo o cada cuánto suceden estos eventos, siguen considerándose notificaciones asíncronas

6.4.1 setTimeout(callback, ms)

Ejecuta una función una sola vez tras un retardo específico. Por ejemplo: para mostrar un mensaje de aviso que desaparece a los pocos segundos

setTimeout(() => {
    console.log("Han pasado 2 segundos");
}, 2000);

6.4.2 setInterval(callback, ms)

Se utiliza para ejecutar una función de manera cíclica, cada vez que transcurre un intervalo de tiempo. Este evento es el más utilizado en los juegos de navegador, es fundamental para programar el "bucle del juego" (game loop)

let segundos = 0;
const idInterval = setInterval(() => {
    segundos++;
    console.log(`Llevas ${segundos} segundos en la página`);
}, 1000);

// Esta función se utiliza para detenerlo
clearInterval(idInterval);