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:
- El usuario interactúa con la aplicación web a través de controles en HTML. Por ejemplo: envía un formulario con datos de inicio de sesión.
- El servidor detecta la interacción del usuario y actúa en consecuencia. Por ejemplo: recibe unos datos enviados a través de un formulario, comprueba si los datos de inicio de sesión son válidos y genera un documento HTML que contiene la respuesta, que será un mensaje de bienvenida o con un error, según el caso. Una vez genera el documento, lo envía de nuevo al usuario.
- El usuario recibe el HTML de respuesta y lo visualiza a través del navegador.
Esta primera aproximación, sin embargo, tiene varios inconvenientes, entre los que destacan:
- Sobrecarga del servidor: cuando el servidor tiene que encargarse de realizar todas las acciones que implica dicha interacción, la carga de trabajo aumenta, por lo que aumenta el consumo de recursos.
- Experiencia del usuario: el usuario percibe que cada elemento interactivo en el que hace click provoca una recarga entera del sitio web. Con un formulario de inicio de sesión parece razonable, pero también sucedería al hacer "like" en Instagram, "retweet" en Twitter, etc.
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:
- Sobrecarga del servidor: al encargarse el navegador del cliente de la representación del sitio web y no tener que generar un HTML nuevo en cada interacción, el servidor reduce drásticamente su carga de trabajo permitiendo, a su vez, la implementación y despliegue de aplicaciones web más complejas.
- Experiencia del usuario: al eliminar la recarga de página, la experiencia se vuelve fluida y reactiva, transformando la web en una verdadera aplicación con interacción en tiempo real.
- Además, se logra una optimización de tráfico en la red: el intercambio de pequeños paquetes de datos es mucho más ligero que transferir archivos HTML enteros desde el servidor hasta el cliente.
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 Itch.io hay una categoría específica de juegos desarrollados en JS https://itch.io/games/tag-javascript
- JS13KGames es una competición anual de desarrollo de juegos en JS en la los desarrolladores deben optimizarlos para que ocupen un máximo de 13KB https://js13kgames.com/
- Hay incluso juegos de VR desarrollados en JS https://aframe.io/showcase/
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:
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:
-
Alto nivel: el programador no debe preocuparse por la gestión de la memoria física o los registros del procesador. Esto implica que no necesita reservar y liberar explícitamente la memoria cuando la necesita o deja de necesitar. En este sentido es similar a Java
-
Interpretado: el código no se transforma a un binario ejecutable, sino que un intérprete lo ejecuta sobre la marcha. Java, sin embargo, sí tiene un proceso de compilación a bytecode para su ejecución en la JVM
Nota: actualmente sí existe un pequeño proceso de compilación de JS conocido como JIT (just-in-time) que ha permitido optimizar muchísimo los tiempos de ejecución
-
De propósito general: aunque nació para otorgar interactividad en los sitios webs, a día de hoy es un lenguaje completo, utilizado en frontend (cliente), en backend (servidor, con un entorno de ejecución llamado Node.js), en aplicaciones móviles (con React Native), en aplicaciones de escritorio (con Electron)... Por su lado, Java también se considera un lenguaje de propósito general. Allá donde puedas instalar JVM podrás ejecutar una aplicación de Java
-
Multiparadigma: es muy flexible, permitiendo crear aplicaciones completamente procedimentales, orientadas a objetos e incluso funcionales. Java, con algunas limitaciones, también se considera multiparadigma
-
Tipado dinámico: no es necesario declarar el tipo de las variables al realizar la asignación de un dato, sino que este dependerá del tipo de dato asignado. Además, es posible cambiar el tipo de una variable en tiempo de ejecución. En este sentido es muy distinto a Java
let dato = 5; // Es un número dato = "Hola"; // Ahora es un texto
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:
-
Opción consola: podemos ejecutar código JS directamente en el navegador web. Entramos en cualquier página web, accedemos a las herramientas de desarrollador, normalmente con F12, con Ctrl + Shift + I o desde el menú de hamburguesa del navegador → Más herramientas → Herramientas para desarrolladores. Tras ello, vamos a la pestaña Console y encontraremos un terminal que procesa código JS. Prueba a poner lo siguiente:
alert('¡Hola, Mundo desde la consola!') -
Opción etiqueta
<script>: dentro de un fichero HTML, podemos añadir las etiquetas<script></script>para incluir código JS. Crea un fichero HTML, copia el siguiente código y pégalo antes de la etiqueta de cierre</body><script> alert('¡Hola, Mundo desde el HTML!'); </script> -
Opción atributo: hay una gran variedad de atributos relacionados con las interacciones del usuario con elementos del documento HTML (eventos). Podemos escribir código directamente dentro de estos atributos para que se ejecute cuando ocurra una acción, como un click. Copia lo siguiente dentro del
<body>de cualquier HTML:<button onclick="alert('¡Hola, Mundo desde el botón!')">Haz clic aquí</button>Esta opción es muy común para pequeñas interacciones rápidas. Cuando se complica su código, lo normal es modularizarlo en una función y llamar a la función. Por ejemplo:
<button onclick="procesarDatos()">Enviar formulario</button>Esa función puede definirse dentro de
<script></script>, o también en un fichero externo -
Opción fichero externo: es la mejor forma de organizar el código, y la más recomendada, ya que mantiene el HTML y el JS separados. Para ello, creamos un fichero con extensión
.js(por ejemplo:script.js) y lo enlazamos en el HTML utilizando el atributosrcde la etiqueta<script>.-
Crea el fichero
script.jscon el siguiente código:alert('¡Hola, Mundo desde el fichero!'); console.log('También te saludo por aquí') -
Enlázalo en tu HTML antes de la etiqueta de cierre
</body><script src="script.js"></script>
-
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:
-
Tipado dinámico: a diferencia de Java, donde debe especificarse el tipo de una variable en el momento de su declaración (por ejemplo:
String nombreoint edad), en JS el tipo se asocia al valor. Es decir: no es necesario indicar el tipo de dato que va a contener una variable. Además, este tipo puede cambiar durante la ejecución del programalet edad = 33; edad = 'Treinta y tres'; -
Tipado débil: el lenguaje realiza coerción de tipos de forma automática. Al intentar hacer operaciones entre diferentes tipos (por ejemplo, número + texto), no detendrá la ejecución ni dará un error. En su lugar, intentará "adivinar" la operación el resultado, haciendo automáticamente conversiones de los valores
1 + '1' // Da como resultado '11' '11' - 1 // Da como resultado 10JS interpreta la primera operación como una concatenación de cadenas de texto, por lo que el resultado es una cadena de texto (lleva comillas). La segunda operación la interpreta como una operación aritmética de sustracción, por lo que el resultado es un número (no lleva comillas). ¿Qué resultado devolverán las siguientes operaciones?
1 + '1' - 1 1 + '1' + 1
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: se utiliza para variables que cambiarán su valor, como contadores en un bucle. Su ámbito es de bloque, por lo que sólo existe dentro del bloque{}donde se declara. Puesto que puede cambiar su valor, JS permite el uso deletpara la declaración de variables sin inicializarconst: es el equivalente alfinalde Java. Se utiliza para valores que no van a ser reasignados. Es la opción que usaremos por defecto. Puesto que no puede cambiar su valor, JS no permite el uso deconstpara la declaración de variables sin inicializar
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:
-
Número (
number): sin importar si es un número decimal o entero. Aunque no definamos ni "veamos" los decimales, en JS todos los números se representan a nivel interno como si fueran números decimales (de punto flotante de 64 bits) -
Cadena de texto (
string): se pueden definir con comillas simples'', dobles""o acentos abiertos``. En JS no existe el tipochar. Un sólo carácter es unstringde longitud 1Nota: el uso de acentos abiertos se utilizan para crear plantillas literales, que facilitan la interpolación de variables y la creación de cadenas multilínea, muy útil para definir plantillas HTML dentro de JavaScript, indispensable cuando lleguemos a la modificación del DOM
-
Booleano (
boolean): sólo admitetrueofalse -
Sin definir (
undefined): es el tipo "por defecto" de una variable, el que recibe cuando no se le asigna ningún valor -
Ausencia de valor (
null): es la ausencia intencionada de valor. En Java, un objeto no inicializado recibe el valornull. En JS,nullsólo se utiliza cuando queremos decir explícitamente que una variable está vacía -
Entero grande (
BigInt): se utiliza para números enteros extremadamente grandes. El tiponumberempieza a perder precisión a partir de 253-1 (o, lo que es lo mismo,Number.MAX_SAFE_INTEGER). En algunos casos puede ser necesario superar ese valor (por ejemplo, para claves criptográficas). Para ello, debemos utilizar el tipoBigInt(añadiendo unanal final del número o haciendo un cambio de tipo explícito conBigInt()). Haz la siguiente prueba:const n = Number.MAX_SAFE_INTEGER; console.log(n); console.log(n+1); console.log(n+2); // Aquí apreciarás que empieza a liarseDato curioso: a pesar de que JS es capaz de operar con números y cadenas (
numberystring), no puede operar connumberyBigInt. El siguiente bloque de código dará error:1n + 1; -
Símbolo (
symbol): tipo de dato más avanzado, utilizado para crear valores únicos e inmutables. Garantizan que, incluso aunque tengan el mismo valor, sean completamente distintos entre sí. Es útil para crear claves que no colisionen entre sí, algo parecido a tener identificadores privados y únicos a nivel de memoriaconst sym1 = Symbol("id"); const sym2 = Symbol("id"); console.log(sym1 === sym2); // Devuelve falseAún no hemos visto
===, pero se trata del operador de identidad
3.1.2.1 null vs undefined
Viniendo desde Java, esta distinción puede ser confusa. Se resume a:
undefined: el lenguaje aún no sabe qué es esto (no se ha definido)null: el programador ha decidido que esto no valga nada (está vacío)
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 quenulles un tipo primitivo, la respuesta detypeofesobject
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):
-
false -
0 -
'' -
undefined -
null -
NaN: valor especial "no es un número" (Not a Number). Es el resultado de una operación matemática que no tiene sentido o que es imposible de realizar con los datos proporcionados, como la raíz cuadrada de un número negativo o la multiplicación de una cadena de texto (en algunos lenguajes de programación, como en Python, se considera "repetición de cadenas", pero JS lo interpreta como una operación aritmética)console.log(Math.sqrt(-1)); console.log('Hola' * 2);Dato curioso: aunque NaN son las siglas de "not a number",
typeof NaNdevolveránumber. ¿En qué quedamos? ¿Es un número o no? Más información aquí
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:
-
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 -
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:
-
En
forclásico: debemos usarlet. La variable contador sí modifica su valor en cada paso del bucle. Si usáramosconst, el programa fallaría al terminar el primer paso del buclefor (let i = 0; i < 10; i++ ) { ... } // Correcto for (const i = 0; i < 10; i++) { ... } // ERROR -
En
for-ofyfor-in: se recomienda usarconst. Parece contradictorio porque cada paso del bucle "cambia" el elemento, pero lo que ocurre en realidad es que en cada iteración se crea un nuevo ámbito (scope) y una nueva variable con el valor actualfor (const elemento of array) { ... } // Correcto for (let elemento of array) { ... } // No da error, pero no es lo ideal
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:
- Palabra clave
function - Nombre de la función
- Lista de parámetros entre paréntesis
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:
- Sin tipos: en ningún momento se indica qué tipo de dato recibe una función ni qué devuelve
- Parámetros opcionales: podemos llamar a una función que pide 3 argumentos con 1 solo. Los otros 2 valdrán
undefined. El programa no explota, solamente sigue - No existe la sobrecarga: si tienes 2 funciones
calcular(a)ycalcular(a, b), la segunda borrará la primera
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:
- Dinámico: no tiene un tamaño fijo. Puedes añadir o eliminar elementos en cualquier momento, como si fuera un
ArrayListde Java - Heterogéneo: pueden almacenar diferentes tipos de datos a la vez (un número, un string, un objeto, un booleano...). Que el lenguaje lo permita no quiere decir que sea recomendable. Por lo general, en los arrays no queremos mezclar tipos de datos
- Multidimensional: pueden contener otros arrays, pudiendo acceder a sus elementos con múltiples índices
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
push(elementos): añade elementos al final del array- Devuelve la longitud nueva
pop(): extrae y elimina el último elemento del array- Devuelve el elemento eliminado (o
undefinedsi el array está vacío)
- Devuelve el elemento eliminado (o
unshift(elementos): añade elementos al principio del array- Devuelve la longitud nueva
shift(): extrae y elimina el primer elemento del array- Devuelve el elemento eliminado (o
undefinedsi el array está vacío)
- Devuelve el elemento eliminado (o
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
indexOf(elemento, desde?): busca la primera ocurrencia del elemento, indicando opcionalmente a partir de qué índice se empieza a buscar- Devuelve el índice de la posición encontrada o
-1si no existe
- Devuelve el índice de la posición encontrada o
includes(elemento, desde?): determina si el array contiene un valor determinado, indicando opcionalmente a partir de qué índice se empieza a buscar- Devuelve un valor booleano (
true/false)
- Devuelve un valor booleano (
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:
-
Notación punto (
.): la más común y similar a Javaconsole.log(profe.nombre); // 'Ignacio' -
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 hacerprofe = {}
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:
- 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 - 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:
- Petición: el navegador solicita o envía datos al servidor
- Serialización: para que los datos viajen por Internet, los objetos que usamos en los programas deben convertirse a cadena de texto (JSON)
- 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:
- Un documento es un objeto JSON (o una variante llamada BSON)
- MongoDB es la BBDD NoSQL más popular. En lugar de tablas y filas utiliza colecciones y documentos
- Sinergia con JS: la comunicación con estas BBDD es completamente natural. No hay que hacer una transformación de datos ni depender de ORMs
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
- Un buen ejemplo es
settings.json, de VS Code. Si abres la configuración de tu cliente de VS Code apreciarás que se guardan en un objeto 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:
- Estructura: podemos crear, eliminar y mover etiquetas existentes en el sitio
- Estilo: podemos añadir y quitar clases de CSS dinámicamente
- Contenido: podemos modificar el texto de un párrafo, el
srcde una imagen...
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:
- Document: ya lo hemos mencionado, es el nodo raíz, el punto de entrada al árbol
- Element: etiquetas HTML propiamente dichas:
h1,p,img,div,span... - Text: contenido de texto dentro de una etiqueta
- Comment: incluso los comentarios HTML forman parte del DOM
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:
input(text, number, date, etc.)textareaselect(para saber quéoptionestá seleccionada)
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:
add('clase'): añade una clase sin tocar las que ya existenremove('clase'): elimina una clase específicatoggle('clase'): si tiene una clase, la quita. Si no la tiene, la añadecontains('clase'): devuelvetruesi tiene la clase ofalsesi no la tiene
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:
- El hilo de ejecución se detiene por completo en la línea en la que se solicitan datos y el procesador no hace "nada" hasta que el usuario pulsa Enter para enviar la línea. Esto es una ejecución síncrona y bloqueante
- El programador decide cuándo se piden los datos, existe un flujo lineal
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:
-
type: el tipo de evento. Por ejemplo:click,mouseover,submit,keydown -
target: referencia al elemento del DOM que originó el evento -
currentTarget: referencia al elemento del DOM que escucha el evento. En nuestros programas "de juguete", será lo mismo quetarget -
timeStamp: momento exacto (en milisegundos) en que se creó el evento -
e.key: en eventos de teclado nos dice qué tecla se ha pulsado. Por ejemplo:'A','Escape'o'ArrowUp' -
preventDefault(): es un método que permite cancelar el comportamiento por defecto del navegador ante ciertos eventos. Por ejemplo: para evitar que un formulario se envíe (provocando la "recarga" de la página en muchos casos) o que un enlace nos lleve a otra páginaconst enlace = document.querySelector('a'); enlace.addEventListener('click', (e) => { e.preventDefault(); // Importante que se haga en la primera línea console.log("Enlace anulado, no tienes escapatoria"); });
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
click: pulsar y soltar el botón izquierdodbclick: doble click con el botón izquierdomouseover: el puntero entra en el área del elemento
6.3.2 Eventos de teclado
keydown: se pulsa una teclakeyup: se suelta una tecla
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);