Introducción al desarrollo web en entorno cliente
1 Introducción
Cuando utilizamos una página web comienza una secuencia de acciones en las que entran en juego distintos programas y servicios (algunos en nuestro equipo y otros fuera de él). La secuencia simplificada sería la siguiente:
- El usuario introduce una URL de la página web que quiere visitar en el navegador.
- Nuestro equipo realizar una petición DNS para conocer la dirección IP a la que realizar la solicitud.
- El navegador solicita la página al servidor.
- El servidor procesa la petición. En el servidor se suelen realizar distintos procesos como:
- Recibir la petición.
- Procesar la petición (puede requerir de un lenguaje de programación: PHP/Ruby/Python/Java…).
- Se accede a una base de datos si es necesario.
- El servidor genera la información que tiene que devolver.
- El servidor devuelve la información.
- El navegador interpreta esa información (código HTML y CSS) y la muestra al usuario.
- Esa información puede contar con parte de información creada en un lenguajes de programación que se ejecute en nuestro navegador.
Los pasos 3 a 5 se repiten también para imágenes, ficheros CSS, JS, vídeos…
Actualmente muchas aplicaciones que antes eran programas de escritorio funcionan completamente desde un navegador:
- Correo electrónico
- Programas de retoque fotográfico
- Gestores empresariales (facturas/almacen/citas)
- Juegos
2 Historia del desarrollo web en entorno cliente
Hoy en día, cuando pensamos en el desarrollo web en el navegador, lo normal es pensar en HTML, CSS y JavaScript. Sin embargo, no siempre ha sido así. A lo largo de la historia se han utilizado diferentes tecnologías para añadir interactividad a las páginas web. Algunas desaparecieron y otras evolucionaron hasta convertirse en los estándares actuales.
2.1 Los primeros años: páginas web estáticas
En 1991 Tim Berners-Lee comienza el desarrollo de HTML en el CERN. No sólo creó el lenguaje HTML si no que creó el protocolo HTTP (HyperText Transfer Protocol) y el primer servidor web.
En 1993 se publica y se propone como estándar por el IETF. En este momento las páginas web estaban formadas únicamente por HTML. Su contenido era completamente estático: mostraban texto, imágenes y enlaces, pero apenas existía interacción con el usuario.
Posteriormente, 1996, apareció CSS, que permitió separar el contenido de la presentación y mejorar el aspecto visual de las páginas.
En esta época, cualquier procesamiento complejo debía realizarse en el servidor, ya que el navegador apenas tenía capacidad de ejecución.
2.1.1 Lenguajes en entorno servidor
Aunque en esta asignatura nos vamos a centrar en el entorno cliente, es interesante conocer que en estos primeros años, para poder hacer las páginas web programando en el backend, se hacía uso de los lenguajes más habituales de la época en entornos UNIX, como son C, Perl y posteriormente Python.
Para usar estos lenguajes de programación se usaba el interfaz CGI (Common Gateway Interface), que permitía a los servidores web ejecutar un programa externo que procesaba la petición HTTP.
El proceso funcionaba de la siguiente manera para cada petición HTTP:
- Crear un nuevo proceso.
- Cargar el programa.
- Ejecutarlo.
- Finalizar el proceso.
Esto hacía que consumiese mucha CPU y recursos de memoria.
El uso de CGI fue antes de los servidores de aplicaciones o módulos en los servidores web.
2.2 JavaScript: el nacimiento de las páginas dinámicas
En 1995, la empresa Netscape desarrolló JavaScript con el objetivo de añadir pequeñas funcionalidades a las páginas web sin necesidad de comunicarse continuamente con el servidor.
Gracias a JavaScript fue posible:
- Validar formularios antes de enviarlos.
- Mostrar u ocultar elementos de la página.
- Crear menús interactivos.
- Responder inmediatamente a las acciones del usuario.
Con el paso de los años, JavaScript fue evolucionando hasta convertirse en un lenguaje de programación muy potente, capaz de desarrollar aplicaciones web completas.
Actualmente es el único lenguaje de programación que todos los navegadores ejecutan de forma nativa.
Netscape liberó el código de su navegador web y creó la organización Mozilla, que a su vez reescribió el navegador que posteriormente se convertiría en Firefox.
2.3 Java Applets: aplicaciones Java en el navegador
Poco después apareció otra tecnología muy popular: los Java Applets. Un Applet era un programa escrito en Java que se descargaba desde una página web y se ejecutaba en el ordenador del usuario mediante una Máquina Virtual de Java (JVM) instalada como complemento del navegador.
Los Applets permitían desarrollar aplicaciones mucho más complejas que las posibles con el JavaScript de aquella época. Por ejemplo:
- Juegos.
- Simulaciones.
- Herramientas de dibujo.
- Aplicaciones educativas.
- Visualizadores en tres dimensiones.
Sin embargo, presentaban varios inconvenientes:
- Era necesario instalar Java en el ordenador.
- Dependían de un complemento del navegador.
- Su tiempo de carga era elevado.
- Presentaban numerosos problemas de seguridad.
En 2017 se declaró obsoleto, y los navegadores dejaron de soportar los complementos necesarios para ejecutar Applets.
2.4 Adobe Flash: las animaciones de Internet
A finales de los años 90 y durante gran parte de los años 2000, Adobe Flash se convirtió en la tecnología dominante para crear contenido multimedia en Internet. Flash permitía desarrollar:
- Animaciones.
- Juegos.
- Reproductores de vídeo.
- Publicidad interactiva.
- Aplicaciones completas.
Durante muchos años fue muy utilizado porque los navegadores todavía no ofrecían muchas de estas capacidades de forma nativa. Sin embargo, Flash tenía varios problemas:
- Requería instalar un complemento.
- Consumía muchos recursos.
- Presentaba frecuentes vulnerabilidades de seguridad.
- No funcionaba correctamente en la gran mayoría de dispositivos móviles.
En 2010 Steve Jobs escribió una carta abierta llamada Reflexiones sobre Flash explicando por qué Apple no permitía Flash en los iPhone. Aunque hubo mucha crítica inicial, y había acusaciones que se podían argumentar contra Apple, años después se confirmaría que Flash consumía muchos recursos, batería y que era inseguro.
En 2020 Adobe dejó de dar soporte a Flash y los navegadores eliminaron completamente su compatibilidad.
2.5 AJAX: aplicaciones más rápidas
En 1999 Microsoft crea la técnica llamada AJAX (Asynchronous JavaScript and XML), como evolución a las etiquetas iframe de 1996, todo ello exclusivo de Internet Explorer 5.0.
Antes de AJAX, cada vez que el usuario realizaba una acción era necesario recargar completamente la página. Gracias a AJAX, el navegador podía comunicarse con el servidor en segundo plano y actualizar únicamente una parte de la página. Esto permitió crear aplicaciones mucho más rápidas y con una mejor experiencia de usuario.
Aunque originalmente utilizaba XML para intercambiar datos, actualmente la mayoría de aplicaciones utilizan el formato JSON.
2.6 jQuery: simplificando el desarrollo web
A partir de 2006, jQuery se convirtió en una de las bibliotecas de JavaScript más populares. En aquella época, desarrollar aplicaciones web con JavaScript puro resultaba complicado porque cada navegador implementaba el DOM y los eventos de forma ligeramente diferente. Además, muchas funcionalidades que hoy consideramos habituales, como querySelector(), fetch() o una gestión uniforme de eventos, todavía no existían o no estaban disponibles en todos los navegadores.
El principal objetivo de jQuery era ofrecer una API sencilla y compatible con todos los navegadores. Gracias a ello, los desarrolladores podían seleccionar elementos utilizando selectores similares a CSS, modificar el contenido de una página, gestionar eventos, crear animaciones o realizar peticiones AJAX sin preocuparse por las diferencias entre navegadores. Esto reducía considerablemente la cantidad de código necesario y facilitaba el desarrollo de aplicaciones web.
Durante varios años, jQuery estuvo presente en la mayoría de los sitios web de Internet y fue una herramienta casi imprescindible para cualquier desarrollador frontend. Su éxito fue tan grande que muchas de las funcionalidades que popularizó terminaron incorporándose de forma nativa a JavaScript y a los navegadores modernos.
Muchas de las funcionalidades que popularizó terminaron incorporándose de forma nativa a JavaScript y a los navegadores modernos.
Con la aparición de HTML5, la estandarización de las APIs del navegador y el desarrollo de frameworks como Angular, React y Vue.js, la necesidad de utilizar jQuery fue disminuyendo. En la actualidad sigue presente en algunos proyectos antiguos, pero en las nuevas aplicaciones web es mucho menos frecuente, ya que los navegadores modernos y los frameworks actuales ofrecen soluciones más completas y potentes.
Aunque jQuery fue la más conocida, durante esos años hubo otras librerías que trataban de extender JavaScript:
- Prototype, de 2005, muy utilizada junto con Ruby on Rails. Añadía métodos al DOM y simplificaba JavaScript.
- script.aculo.us, creada sobre Protoype, era una librería para crear animaciones, efectos visuales y drag & drop.
- Ext JS: Orientada a aplicaciones empresariales con interfaces muy similares a programas de escritorio. Sigue existiendo, aunque con un uso mucho menor.
2.7 HTML5: el navegador gana capacidades
En 2007 se publicó el primer borrador de HTML5, una importante evolución del lenguaje HTML. HTML5 incorporó numerosas funcionalidades que antes solo podían conseguirse mediante tecnologías externas como Flash.
Gracias a HTML5, los navegadores modernos comenzaron a convertirse en una plataforma de desarrollo mucho más completa gracias a que incorporaron APIs para ejecutar de forma nativa:
- Reproducción de vídeo y audio sin complementos (Youtube empezó a usarlo como reproductor para el iPhone).
- Dibujos mediante la etiqueta <canvas>.
- Gráficos vectoriales con SVG.
- Almacenamiento local de datos.
- Geolocalización.
- APIs para acceder a diferentes recursos del dispositivo.
2.8 Frameworks de JavaScript
A medida que las aplicaciones web crecían en tamaño, desarrollar todo el código únicamente con JavaScript resultaba cada vez más complicado. Por este motivo aparecieron diferentes frameworks, cuyo objetivo era facilitar el desarrollo de aplicaciones complejas.
Algunos de los más conocidos son:
Estos frameworks permiten organizar el código, reutilizar componentes y desarrollar aplicaciones de gran tamaño de forma más sencilla.
Hoy en día también existe TypeScript, que es un lenguaje de programación de alto nivel que se transpila a JavaScript. Es un superset de JavaScript. Hoy en día distintos frameworks y aplicaciones lo utilizan. El código TypeScript no se ejecuta en entorno cliente.
2.9 WebAssembly: ejecución de binarios en el navegador
En los últimos años ha aparecido una nueva tecnología llamada WebAssembly (Wasm). WebAssembly no es un lenguaje de programación, sino un formato binario que los navegadores pueden ejecutar de forma muy eficiente. Permite utilizar lenguajes de programación como C, C++, Rust, Java que se compilan para desarrollar aplicaciones que posteriormente se ejecutan dentro del navegador.
Esta tecnología es especialmente útil para aplicaciones que requieren un alto rendimiento, como:
- Videojuegos.
- Editores de imagen.
- Modelado 3D.
- Simulaciones científicas.
- Edición de vídeo.
Aunque WebAssembly puede ejecutar código compilado muy rápidamente, JavaScript sigue siendo necesario para interactuar con el navegador y con la página web.
3 Arquitectura cliente-servidor
La mayoría de aplicaciones web utilizan una arquitectura denominada cliente-servidor. Está formada por dos partes claramente diferenciadas.
En la siguiente tabla se puede ver un resumen de las diferencias existentes entre el entorno cliente y el servidor:
| Cliente | Servidor | |
|---|---|---|
| Puede ser | ● Ordenador / móvil / tablet … ● Navegador Web |
● Servidor remoto |
| Función | ● Solicitar información. ● Gestiona la interfaz. ● Ejecutar código cliente. ● Recoger las acciones del usuario. ● “Sólo lo usa un usuario” |
● Recibir peticiones. ● Gestiona la lógica de negocio. ● Acceder a bases de datos. ● Enviar respuestas al cliente. ● Puede atender miles de usuarios. |
| Tecnologías | ● HTML ● CSS ● Javascript ● WebAssembly (Wasm) |
● PHP ● Python ● Ruby on Rails ● Java ● NodeJS |
En la siguiente imagen se puede ver una simplificación de la arquitectura:
En la imagen podemos diferenciar:
- Cliente: Formado por nuestro equipo y el navegador web que usemos.
- Servidor: Una aplicación web moderna suele contar con:
- Servidor Web: Apache HTTP Server o Nginx, por poner dos ejemplos muy conocidos.
- Lenguaje de programación entorno servidor / framework
- Base de datos: en las bases de datos relacionales las más conocidas son: MySQL, MariaDB o PostgreSQL.
3.1 Tecnologías del lado del cliente
Tal como hemos visto previamente, actualmente existen tres tecnologías fundamentales que se usan en el entorno cliente:
- HTML: define la estructura de una página.
- CSS: define el aspecto visual.
- JavaScript: es un lenguaje de programación que se ejecuta en el navegador Web de forma nativa que añade comportamiento, permite modificar el aspecto visual de manera interactiva, puede realizar peticiones al servidor de manera asíncrona, …
Más adelante nos adentraremos más en ellos.
5 Herramientas para desarrollar
Aunque a lo largo del curso usaremos distintas herramientas, al inicio de curso sólo haremos uso de unas pocas. Para empezar haremos uso de:
- Editor: Visual Studio Code (extensiones recomendadas: ESLint, Prettier, Live Server).
- Git como sistema de control de versiones.
- GitHub como repositorio remoto.
- Navegador web: Firefox, Chrome, …
- Herramientas de desarrollador: Todos los navegadores incluyen herramientas para facilitar el desarrollo. Normalmente se abren pulsando F12 o Ctrl + Shift + I.
Introducción a JavaScript
6 Historia de JavaScript
Actualmente, JavaScript es el lenguaje de programación más utilizado para desarrollar aplicaciones web. Está presente en prácticamente todas las páginas de Internet y es capaz de ejecutarse tanto en navegadores web como en servidores y otros entornos.
Creado en 1995 por la empresa Netscape JavaScript no nació con el objetivo de desarrollar aplicaciones complejas. Su propósito inicial era mucho más modesto: añadir pequeñas funcionalidades a páginas HTML estáticas.
Con el paso del tiempo, el lenguaje ha evolucionado hasta convertirse en una plataforma completa para desarrollar aplicaciones web, aplicaciones de escritorio, aplicaciones móviles e incluso programas para servidores.
El nombre JavaScript fue elegido principalmente por motivos comerciales, aprovechando la enorme popularidad que Java estaba adquiriendo en aquella época. A pesar del nombre, JavaScript y Java son lenguajes completamente diferentes. Comparten una sintaxis superficial parecida, pero tienen objetivos, características y formas de ejecución distintas.
Durante la segunda mitad de los años 90 comenzó la denominada guerra de los navegadores. Cada fabricante añadía nuevas características a JavaScript sin seguir un estándar común. Como consecuencia:
- El mismo programa podía funcionar en un navegador y fallar en otro.
- Los desarrolladores debían escribir código específico para cada navegador.
- El mantenimiento de las aplicaciones era complicado.
Esta situación hizo evidente la necesidad de crear un estándar.
6.1 La estandarización del lenguaje
En 1997 la organización ECMA International publicó la primera especificación oficial del lenguaje que recibió el nombre de ECMAScript. A partir de ese momento, los navegadores comenzaron a implementar el mismo estándar, reduciendo progresivamente las diferencias entre ellos. Actualmente todos los navegadores modernos implementan las especificaciones publicadas por ECMAScript.
7 ECMAScript
Cuando hablamos de JavaScript es habitual utilizar indistintamente los términos JavaScript y ECMAScript. Sin embargo, no significan exactamente lo mismo.
- ECMAScript es la especificación o estándar que define cómo debe funcionar el lenguaje. La especificación se puede leer en ECMAScript® 2026 Language Specification.
- JavaScript es la implementación de ese estándar realizada por los diferentes motores de ejecución.
Cuando usamos un navegador web estaremos haciendo uso del motor de ejecución (JavaScript Engine) que tenga implementado:
- SpiderMonkey: creao por la fundación Mozilla y utilizado en Firefox y derivados, pero también en MongoDB y en el entorno de escritorio GNOME.
- V8: creado por Google en 2008 para ser usado en Chrome. También se usa en entorno servidor, principalmente en Node.js y en las aplicaciones basadas en Electron.
- JavaScriptCore: utilizado en Safari.
Dentro de ECMA existe un comité denominado TC39, responsable de estudiar, diseñar y aprobar las nuevas características del lenguaje. Entre sus funciones destacan:
- Proponer nuevas funcionalidades.
- Analizar su viabilidad.
- Debatir posibles mejoras.
- Aprobar las modificaciones del estándar.
El objetivo es garantizar que JavaScript evolucione de forma ordenada y compatible con versiones anteriores.
7.1 Evolución
En 2009 apareció una nueva versión del lenguaje denominada ECMAScript 5 (ES5). Esta versión mejoró considerablemente la estabilidad y la compatibilidad entre navegadores. Sin embargo, el cambio más importante llegó en 2015 con la publicación de ECMAScript 2015 (ES6), que transformó el lenguaje.
Desde 2015, cada año se publican nuevas versiones del estándar incorporando pequeñas mejoras y nuevas funcionalidades. En la Wikipedia se puede ver el historial de versiones.
7.2 Línea temporal
| Año | Acontecimiento |
|---|---|
| 1991 | Nace la World Wide Web. |
| 1995 | Brendan Eich crea JavaScript para Netscape Navigator. |
| 1997 | Se publica la primera especificación ECMAScript. |
| 1999 | ECMAScript 3 se convierte en el estándar dominante durante varios años. |
| 2005 | Se populariza AJAX y comienzan a aparecer aplicaciones web más dinámicas. |
| 2006 | jQuery simplifica el desarrollo con JavaScript. |
| 2009 | Se publica ECMAScript 5. Mejoras en los objetos, modo “estricto”, mejoras en JSON, … |
| 2015 | ECMAScript 2015 (ES6) introduce una profunda modernización del lenguaje. Variables let y const, clases, módulos, promesas, parámetros por defecto… |
| 2016-Actualidad | Publicación anual de nuevas versiones de ECMAScript. |
7.3 Compatibilidad entre navegadores
Debido a la evolución del lenguaje, los motores de ejecución y navegadores no tienen por qué estar siempre a la última, o pueden priorizar las distintas nuevas características. Para consultar las compatibilidades de los navegadores podemos usar las siguientes webs:
- Mozilla Developer Network: La documentación oficial más utilizada por los desarrolladores.
- ECMAScript Compatibility Table: Para consultar la compatibilidad de las características del lenguaje JavaScript.
- Can I use: para consultar HTML, CSS, APIs del navegador, …
8 Características de JavaScript
JavaScript es un lenguaje interpretado. Esto significa que el código fuente no necesita compilarse previamente para ejecutarse. Cuando el navegador carga una página web, interpreta y ejecuta el código JavaScript.
Actualmente, los motores JavaScript modernos utilizan técnicas mucho más avanzadas, como la compilación Just-In-Time (JIT), que traducen parte del código a lenguaje máquina mientras el programa se está ejecutando para mejorar el rendimiento.
Una de las mayores ventajas de JavaScript es que puede ejecutarse en diferentes sistemas operativos sin modificar el código, siempre que haya un motor JavaScript en el SO.
JavaScript es un lenguaje de tipado dinámico. Esto significa que una variable puede almacenar distintos tipos de datos a lo largo de la ejecución del programa. Y es tipado débil, ya que el propio lenguaje realiza conversiones automáticas entre distintos tipos de datos cuando lo considera necesario. Estas conversiones automáticas facilitan algunas operaciones, pero también pueden generar resultados inesperados si no se tiene cuidado.
Es recomendable realizar conversiones explícitas cuando sea necesario
Permite desarrollar programas utilizando programación orientada a objetos, aunque se implementa de forma diferente a otros lenguajes como Java o C#.
Una característica interesante es que las funciones son tratadas como cualquier otro valor, lo que permite:
- Almacenarse en variables.
- Enviarse como parámetros.
- Devolverse desde otras funciones.
- Formar parte de objetos.
Esta característica resulta fundamental para comprender conceptos como los eventos, los temporizadores o la programación asíncrona.
Como otros lenguajes interpretados, incorpora un sistema de recolección automática de memoria (Garbage Collector). El programador no necesita liberar manualmente la memoria utilizada por los objetos que ya no son necesarios. El motor JavaScript detecta automáticamente qué recursos ya no están siendo utilizados y los libera. Esto reduce considerablemente la posibilidad de producir errores relacionados con la gestión de memoria.
Una de las características más importantes de JavaScript moderno es su capacidad para trabajar de forma asíncrona. Esto permite ejecutar determinadas tareas sin bloquear el resto del programa. Por ejemplo:
- Descargar datos de Internet.
- Esperar la respuesta de un servidor.
- Leer archivos.
- Ejecutar temporizadores.
Gracias a ello, las aplicaciones web pueden seguir respondiendo a las acciones del usuario mientras esperan la finalización de otras operaciones.
8.1 Seguridad y limitaciones
JavaScript fue diseñado para ejecutarse en el navegador del usuario, por lo que se ejecuta en el ordenador o dispositivo del usuario y no en el servidor. Esta característica ofrece ciertas ventajas, como una mayor interactividad y una respuesta más rápida de las aplicaciones. Sin embargo, también plantea importantes retos relacionados con la seguridad.
Los navegadores modernos implementan medidas para impedir que una página web pueda acceder libremente al ordenador del usuario o interferir con otras páginas abiertas.
8.2 Entorno aislado (Sandbox)
Una de las principales medidas de seguridad consiste en ejecutar el código JavaScript dentro de un entorno aislado, conocido como sandbox (caja de arena). El sandbox limita las acciones que puede realizar un programa JavaScript, evitando que tenga acceso directo al sistema operativo. Gracias a este mecanismo, una página web no puede, por ejemplo:
- Leer cualquier archivo del disco duro.
- Modificar archivos del sistema.
- Instalar programas.
- Ejecutar aplicaciones del sistema operativo.
- Acceder libremente a la información de otras aplicaciones.
- Acceder al hardware directamente. Antes de acceder a recursos (cámara, microfono, ubicación, …) pedirá autorización al usuario.
Estas restricciones protegen al usuario frente a posibles páginas maliciosas.
8.3 Política del mismo origen (Same-Origin Policy)
Una medida de seguridad muy importante es la Política del mismo origen (Same-Origin Policy). Esta política impide que una página web pueda acceder libremente a la información de otra página perteneciente a un sitio diferente. Si el usuario tiene abiertas simultáneamente dos páginas distintas, el código de una de ellas no podrá acceder al contenido de la otra.
8.4 Restricciones en las peticiones HTTP
Las aplicaciones web suelen comunicarse con servidores mediante peticiones HTTP. Sin embargo, los navegadores también aplican restricciones a estas comunicaciones. Cuando una aplicación intenta acceder a un servidor diferente al que ha servido la página, el servidor remoto debe autorizar expresamente dicha comunicación mediante una técnica denominada CORS (Cross-Origin Resource Sharing).
Si el servidor no está configurado para permitir el acceso, el navegador bloqueará la petición. Esta medida evita que páginas maliciosas accedan a servicios web sin autorización.
8.5 El código JavaScript es visible
A diferencia de otros lenguajes utilizados en el servidor, el código JavaScript llega al navegador del usuario. Cualquier persona puede visualizarlo utilizando las herramientas de desarrollo del navegador, y por lo tanto incluso modificarlo. Por este motivo:
- No deben incluirse contraseñas.
- No deben almacenarse claves privadas.
- No deben incorporarse datos confidenciales.
- No deben implementarse mecanismos de seguridad basados únicamente en código JavaScript.
Todo el código enviado al navegador debe considerarse público, aunque existen herramientas para ofuscarlo.
8.5.1 La validación debe realizarse también en el servidor
Una aplicación puede utilizar JavaScript para comprobar que un formulario está correctamente cumplimentado: validar e-mail, asegurar que un campo está rellenado, …
Teniendo en cuenta el punto anterior, estas comprobaciones no son suficientes ya que el usuario podría desactivar el JavaScript, o modificar el propio código, o modificar las peticiones realizadas al servidor. Por ello, todas las validaciones importantes deben repetirse siempre en el servidor.
Todas las validaciones importantes deben repetirse siempre en el servidor
La validación realizada en JavaScript mejora la experiencia del usuario, pero nunca debe sustituir a la validación del lado del servidor.
La validación realizada en JavaScript mejora la experiencia del usuario, pero nunca debe sustituir a la validación del lado del servidor.
Conceptos básicos de JavaScript
9 Escribir código JavaScript
JavaScript puede incorporarse a una página web de diferentes formas. La elección de una u otra dependerá del tamaño del proyecto, de la organización del código y de las necesidades de la aplicación. En este apartado aprenderemos las distintas formas de incluir código JavaScript en un documento HTML y las buenas prácticas recomendadas en el desarrollo web moderno.
Existen dos formas principales de incorporar JavaScript a una página web:
- Código integrado (interno).
- Código en un fichero externo.
9.1 Código integrado
El código integrado consiste en escribir el código JavaScript directamente dentro del documento HTML mediante la etiqueta <script>.
Código JavaScript integrado en HTML
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Mi primera página</title>
</head>
<body>
<h1>Hola Mundo</h1>
<!-- esto es un comentario HTML -->
<script>
// esto es un comentario en JavaScript
console.log("Hola desde JavaScript");
</script>
</body>
</html>
Cuando el navegador encuentra la etiqueta <script>, interpreta y ejecuta inmediatamente el código contenido en su interior. Dentro de la misma página web puede haber varias etiquetas, que se ejecutarán en orden.
La etiqueta <script> puede tener un atributo type, como type="text/javascript". Si no se pone, se asume como código JavaScript por defecto.
9.2 Código en un fichero externo
La forma más habitual a la hora de escribir código JavaScript es en un fichero independiente con extensión .js, por ejemplo js/app.js. Desde el fichero HTML se referencia el archivo JavaScript.
Código index.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Mi primera página</title>
</head>
<body>
<h1>Hola Mundo</h1>
<script src="js/app.js"></script>
</body>
</html>
Código js/app.js
console.log("Hola desde un fichero externo");
Cuando el navegador recibe el código HTML lo analizará y verá el atributo src, por lo que realizará una nueva petición al servidor para descargar el fichero y así poder ejecutarlo. Es la opción recomendada en prácticamente cualquier proyecto ya que cuenta con unas ventajas que el código integrado no tiene:
- El código HTML es más limpio.
- Se separa la estructura de la lógica.
- El mismo fichero puede reutilizarse en varias páginas.
- Es más sencillo localizar errores.
- Facilita el trabajo en equipo.
- El navegador puede almacenar el archivo en caché para futuras visitas. No volverá a pedirlo al servidor.
Cuando un proyecto crece puede ser recomendable separar el código en distintos ficheros.
Escribe el código anterior y busca dónde aparece el log.
9.2.1 Carga de varios ficheros
Cuando una página contiene varios archivos JavaScript, el orden en el que se cargan puede ser muy importante. El navegador ejecuta los scripts siguiendo el orden en que aparecen en el documento HTML. Teniendo esto en cuenta, es importante tener en cuenta si existen dependencias entre ficheros.
Supongamos los siguientes dos ficheros:
Código js/funciones.js
function saludar(nombre) {
console.log(`Hola ${nombre}`);
}
Código js/app.js
saludar("Bob");
9.3 ¿Dónde cargar el JavaScript?
Tradicionalmente existían dos posibilidades.
- Dentro del []{verbatim}: Este método puede provocar problemas. Mientras el navegador descarga deja de construir la página HTML. Como consecuencia, el usuario puede percibir una carga más lenta, y si realiza alguna ejecución, como el HTML no está construido, puede haber errores.
- Antes de cerrar []{verbatim}: Durante muchos años la recomendación fue colocar los scripts justo antes del cierre del elemento []{verbatim}.
Código en el head
<head>
<script src="03.js"></script>
</head>
<body>
<h1 id="titulo">Título</h1>
Antes de cerrar body
<body>
<h1 id="titulo">Título</h1>
...
<script src="03.js"></script>
</body>
¿Qué diferencia hay al cargar el siguiente código en el head o al terminar el body?
console.log(document.getElementById("titulo"))
9.3.1 Atributo defer
Actualmente, la práctica más recomendada consiste en utilizar el atributo defer. Esto mejora el rendimiento y evita muchos errores relacionados con elementos HTML que todavía no existen, ya que:
- el navegador continúa construyendo el documento HTML;
- el archivo JavaScript se descarga en segundo plano;
- el código no se ejecuta hasta que el documento HTML ha terminado de cargarse.
Usando defer como atributo
<head>
<script src="app.js" defer></script>
</head>
Modifica el ejercicio anterior usando defer en el head.
9.3.2 Atributo async
También existe el atributo async. En este caso el navegador descarga el archivo en paralelo, pero lo ejecuta tan pronto como termina la descarga, incluso aunque el documento HTML todavía no haya finalizado. El orden de ejecución entre varios scripts con async no está garantizado.
Por ello suele utilizarse únicamente para scripts independientes, como ejemplos:
- Herramientas de análisis de visitas: suelen ser librerías que funcionan aisladas de la aplicación. Recopilan datos y los envían a servidores externos (como Google Analytics).
- Publicidad: similar al caso anterior, pero para la inclusión de publicidad. En este caso obtienen datos de un servidor externo para incrustar las imágenes en la web.
No es recomendable utilizar async para scripts que dependen unos de otros.
9.4 Comparación y buenas prácticas
Tal como se ha podido ver, existen distintas maneras a la hora de cargar el código JavaScript, por lo que es importante entender dónde poner el código y que quizá sea necesario usar atributos al cargar los ficheros.
En la siguiente imagen se puede ver una representación visual de los distintos métodos de carga:
En la siguiente tabla se resumen también cómo se pueden usar.
| Método | ¿Bloquea la carga del HTML? | ¿Respeta el orden? | Uso recomendado |
|---|---|---|---|
| <script> clásico | Sí | Sí | Solo ejemplos sencillos. |
| <script defer> | No | Sí | Opción recomendada. |
| <script async> | No | No necesariamente | Scripts independientes. |
Es importante conocer las diferencias de cómo cargar nuestro código.
9.5 Gramática del lenguaje
Como todos los lenguajes de programación, JavaScript cuenta con su propia gramática y léxico. En la documentación de MDN se explica el léxico completo.
10 Modo estricto ("use strict")
JavaScript fue diseñado originalmente para ser un lenguaje muy flexible. Durante sus primeros años permitía realizar determinadas acciones que podían dar lugar a errores difíciles de detectar, como crear variables sin declararlas o utilizar construcciones poco recomendables. Para mejorar la seguridad y facilitar el desarrollo de aplicaciones, ECMAScript 5 (ES5) introdujo en 2009 el modo estricto (Strict Mode).
El modo estricto se activa escribiendo la siguiente instrucción al comienzo de un fichero o de una función:
Añadiendo modo estricto
"use strict";
A partir de ese momento el intérprete aplica un conjunto de reglas más estrictas que ayudan a detectar errores durante el desarrollo. Por ejemplo, impide crear variables de forma accidental, evita determinadas operaciones consideradas inseguras y genera excepciones en situaciones que anteriormente podían pasar desapercibidas. Esto facilita la detección de errores y contribuye a escribir código más robusto y mantenible.
En la actualidad, los módulos ECMAScript (ficheros que utilizan import y export) funcionan automáticamente en modo estricto, por lo que no es necesario escribir la cabecera en ellos. Sin embargo, sigue siendo recomendable conocer esta directiva, ya que continúa apareciendo en numerosos proyectos y permite comprender el funcionamiento de aplicaciones desarrolladas antes de la popularización de los módulos.
A lo largo de este libro utilizaremos siempre las características modernas de JavaScript y seguiremos las recomendaciones actuales del lenguaje. Aunque en muchos proyectos modernos el modo estricto ya se activa automáticamente, conocer su finalidad ayuda a comprender la evolución de JavaScript y la importancia de adoptar buenas prácticas desde el comienzo del desarrollo.
Crea un fichero JavaScript con el siguiente código. ¿Qué sucede?
Uso estricto
"use strict";
nombre = "Ana";
console.log(nombre);
11 Comentarios
Los comentarios son fragmentos de texto incluidos en el código fuente que no son ejecutados por el motor de JavaScript. Su finalidad es ayudar a comprender el funcionamiento del programa, documentar decisiones de diseño o desactivar temporalmente determinadas instrucciones durante el desarrollo.
Al igual que sucede en otros lenguajes podemos hacer comentarios de una única línea o de varias.
Comentarios de una línea
// Este es un comentario
console.log("Hola mundo");
let edad = 20; // edad
Comentando varias líneas
/*
Este comentario puede ocupar
una o varias líneas.
*/
console.log("Hola");
Los comentarios deberían aportar información útil al código, por lo que no se debería usar para explicar cualquier línea. Lo habitual es usarlos para:
- Algoritmos complejos.
- Decisiones de diseño.
- Soluciones poco evidentes.
- Limitaciones conocidas.
- Código pendiente de modificar.
Los comentarios deben mantenerse actualizados a medida que se actualiza el código.
11.1 Comentarios JSDoc
Al igual que sucede en otros lenguajes de programación, en proyectos profesionales es habitual utilizar un formato especial de programación. En el caso de Java se suele usar Javadoc y para JavaScript, con sintaxis similar, se puede usar JSDoc.
Permite documentar funciones, clases y otros elementos del programa de forma estructurada. Su sintaxis comienza con /**.
Comentarios con JSDoc
/**
* Devuelve la suma de dos números.
*
* @param {number} a Primer número.
* @param {number} b Segundo número.
* @returns {number} Resultado de la suma.
*/
function sumar(a, b) {
return a + b;
}
12 Variables
Las variables permiten almacenar información en memoria para poder utilizarla posteriormente. Gracias a ellas un programa puede recordar valores, realizar cálculos, modificar información o intercambiar datos entre distintas partes del código.
Crear y usar una variable
let nombre = "Alice";
console.log(nombre);
En este caso:
- let: indica que vamos a declarar una variable.
- nombre: es el identificador.
- "Alice": es el valor almacenado.
- console.log(nombre);: uso de la variable
El nombre de una variable recibe el nombre de identificador y debe cumplir una serie de reglas. Puede contener:
- Letras.
- Dígitos (excepto al principio).
- Guion bajo (_).
- Símbolo dólar ($).
Lógicamente no se pueden usar palabras reservadas como identificador de variables. También es importante saber que JavaScript es case sensitive, por lo que nombre no es lo mismo que Nombre.
JavaScript es case sensitive. Cuidado a la hora de usar identificadores.
12.1 Declaración e inicialización
Es importante distinguir dos conceptos.
- Declarar la variable: Consiste en crear la variable. Todavía no tiene un valor.
- Inicializar la variable: Consiste en asignarle un valor por primera vez.
- Ambas acciones e pueden hacer a la vez.
Declarar
let edad;
Inicializar
edad = 20;
Ambas
let edad = 20;
12.2 Diferencias entre let, const y var
A la hora de declarar variables podemos usar tres palabras reservadas:
- let: Es la forma recomendada para declarar variables cuyo valor puede cambiar.
- const: Declara una constante. Una constante debe recibir un valor en el momento de su creación. Cuando una constante almacena un objeto o un array, lo que permanece constante es la referencia al objeto, no necesariamente su contenido. Por convenio, cuando una constante representa un valor fijo global suele escribirse en mayúsculas.
- var: Antes de ECMAScript 2015 era cómo se declaraban, pero no se recomienda utilizarla en código nuevo. El motivo es que presenta un comportamiento diferente respecto al ámbito de las variables y a la elevación (hoisting), lo que puede provocar errores difíciles de detectar.
Variable con let
let contador = 0;
contador++;
contador++;
console.log(contador);
Constantes
const PI = 3.141592;
PI = 3; // TypeError:
const letras = ["a", "b"];
// permitido
letras.push("c");
Con var
var nombre = "Ana";
12.3 Ámbito de las variables
El ámbito (scope) determina desde qué partes del programa puede accederse a una variable. Existen cuatro ámbitos:
- Global scope: El ámbito por defecto para todo el código en modo script.
- Module scope: El ámbito para el código de módulos (lo veremos más adelante).
- Function scope: El ámbito dentro de una misma función.
- Block scope: Para variables let y const el ámbito que hay entre llaves (un bloque).
Sirva como resumen la siguiente tabla:
| let | const | var | |
|---|---|---|---|
| Ámbito (scope) | Bloque | Bloque | Función/Global |
| Re-declaración permitida | Sí | No | Sí |
| Re-asignación permitida | Sí | No | Sí |
| Uso recomendado | Para variables que cambian | Para valores fijos o referencias | Evitarlo |
Es importante tener en cuenta el scope de las variables para evitar errores.
12.4 Convención de nombres
JavaScript permite utilizar muchos nombres distintos, pero existen convenciones ampliamente aceptadas.
- camelCase: Es la convención utilizada en JavaScript. La primera palabra comienza en minúscula y las siguientes empiezan con mayúscula. Ejemplo de nombres: nombreCompleto, numeroAlumnos, fechaNacimiento, …
- Snake_Case: Usado en Python, PHP, … Ejemplo de nombres: nombre_completo, numero_alumnos, …
Por otro lado, habría que decidir el idioma de las variables, cómo poner los comentarios… todo depende del idioma usado por los programadores, si el código va a ser publicado…
Lo importante es elegir una convención y tratar de mantenerla siempre.
13 Tipos de datos
Toda la información que maneja un programa se almacena en forma de datos. JavaScript puede trabajar con distintos tipos de datos, como números, textos, valores lógicos, listas de elementos u objetos. Conocer los distintos tipos de datos es fundamental para escribir programas correctos y evitar errores.
JavaScript es un lenguaje de tipado dinámico, lo que significa que no es necesario indicar el tipo de una variable al declararla. Al contrario que pasa en Java o C, que es el programador el encargado de indicar el tipo de variable, en JavaScript será el propio lenguaje quien determine automáticamente el tipo a partir del valor asignado.
13.1 Tipos primitivos
Como otros lenguajes de programación, JavaScript cuenta con unos tipos de datos primitivos, que representan un valor:
- Boolean: Valores que sólo pueden ser true y false.
- null: Palabra especial que determina que es un valor nulo.
- undefined: Valor primitivo asignado a las variables que no se han declarado todavía.
- Number: Número entero o de coma flotante. Por ejemplo: 42 o 3.14159.
- Puede resultar en valores Infinity y -Infinity que representan el valor máximo y mínimo posibles.
- NaN: Not a Number, el resultado de saber si un tipo de datos es un número y resultao que no lo es: Number("hola")
- BigInt: Un entero de precisión arbitraria. Por ejemplo: 9007199254740992n.
- String: Una secuencia de caracteres que forman un texto. Se puede escribir con:
- Comillas simples: 'hola'
- Comillas dobles: "hola"
- Comillas invertidas: `hola`
- Symbol: Es un tipo de datos que garantiza que es único e inmutable.
Ejemplo tipos de datos
let verdadero = true;
let falso = false;
let nulo = null;
let edad = 18;
let temperatura = 22.5;
let saldo = -150.75;
// Infinity
console.log(42 / 0);
// -Infinity
console.log(-42 / 0);
Ejemplo tipos de datos
let nombre = "Charlie";
let ciudad = 'Bilbao';
let saludo = `Hola`;
console.log("Hola "+nombre)
console.log("Hola %s",nombre)
console.log(`Hola ${nombre}`)
let edad;
// undefined
console.log(edad);
13.2 Objetos
El resto de tipos de datos no descritos previamente son de tipo Object, entre los que se pueden incluir funciones, arrays, expresiones regulares, Map o objetos de tipo Error.
Un objeto ejemplo de objeto sería el siguiente:
Ejemplo de Object
const alumno = {
nombre: "Alice",
edad: 20,
aprobado: true
};
console.log(alumno.nombre)
Más adelante profundizaremos en la sección de Objetos.
13.3 Operador typeof
JavaScript incorpora el operador typeof, que permite conocer el tipo de un dato.
Operador typeof
let nombre = "Bob";
typeof nombre;
13.4 Conversión de tipos
Muchas veces es necesario convertir un tipo de datos a otro, puede que de manera temporal o en otras ocasiones de manera permanente. Se va a explicar la conversión explícita, ya que existen algunos casos que se van a explicar después con los operadores.
Conversión de datos
let edad = Number("25");
//NaN
Number("Hola")
parseInt("123")
// 123
parseInt("123px")
parseInt(123.4)
parseFloat("12.75")
Conversión de datos
String(25)
String(true)
Boolean(1)
Boolean("123")
Boolean(0)
Boolean("")
Boolean(null)
Boolean(undefined)
Boolean(NaN)
Haz una prueba de las distintas conversiones de tipos vistas.
13.5 Comparación entre == y ===
Debido a las conversiones automáticas que realiza JavaScript por defecto, hace que se vea especialmente afectados los operadores de comparación. Esto hace que a veces podamos tener fallos en nuestro código no deseados:
Para evitar las conversiones automáticas existe el operador === o strict equality que devuelve un Boolean y tiene en cuenta tanto el valor como el tipo de datos.
¿Qué resultados dan la siguientes operaciones?
- 5 === 5
- 5 === "5"
- 1 === true
- 0 === false
- Symbol(42) === Symbol(42)
- typeof Symbol(42) === typeof Symbol(42)
El operando de negación es !==.
Es recomendable hacer uso de === a la hora de realizar comparaciones.
14 Operadores
Los operadores permiten realizar operaciones sobre uno o varios valores. Gracias a ellos es posible realizar cálculos matemáticos, comparar datos, combinar condiciones, asignar valores o modificar variables. No vamos a entrar en todos ellos, ya que son muchos, pero todos ellos se encuentran en la documentación de MDN y están diferenciados por “familias” (de asignación, comparación, aritméticos…).
14.1 Operadores aritméticos
Los operadores aritméticos permiten realizar operaciones matemáticas.
| Operador | Descripción | Ejemplo |
|---|---|---|
+ |
Suma | 5 + 2 |
- |
Resta | 5 - 2 |
* |
Multiplicación | 5 * 2 |
/ |
División | 5 / 2 |
% |
Resto de la división | 5 % 2 |
** |
Potencia | 5 ** 2 |
14.2 Operadores de asignación
Permiten almacenar un valor en una variable. Aunque el operador más utilizado es = también existen otros operadores que nos permiten escribir de operaciones de manera abreviada.
| Operador | Equivale a |
|---|---|
+= |
x = x + valor |
-= |
x = x - valor |
*= |
x = x * valor |
/= |
x = x / valor |
%= |
x = x % valor |
**= |
x = x ** valor |
Escribe un ejemplo para cada una de las asignaciones anteriores.
14.3 Operadores de incremento y decremento
Permiten aumentar o disminuir una variable en una unidad. Pueden colocarse antes o después de la variable, y depende de dónde se pongan varía su comportamiento.
Incremento
let x = 5;
x++;
console.log(x++);
console.log(++x);
Decremento
let x = 5;
x--;
console.log(x--);
console.log(--x);
¿Cuál es el resultado del código anterior?
14.4 Operadores de comparación
Permiten comparar dos valores. El resultado siempre es un valor booleano: true o false.
| Operador | Significado |
|---|---|
| > | Mayor que |
| < | Menor que |
| >= | Mayor o igual |
| <= | Menor o igual |
| == | Igual (con conversión automática) |
| != | Distinto (con conversión automática) |
| === | Igual estricto |
| !== | Distinto estricto |
Como ya vimos en el apartado anterior, se recomienda utilizar === y !==.
14.5 Operadores lógicos
Permiten combinar varias condiciones. Los usaremos más adelante en las sentencias condicionales.
| Operador | Significado |
|---|---|
&& |
Y lógico (AND) |
|| |
O lógico (OR) |
| ! | Negación (NOT) |
14.6 Operador de concatenación
El operador + también puede utilizarse para unir cadenas de texto, pero actualmente suele preferirse el uso de template literals.
Concatenar texto
const nombre = "John";
const apellido = "Doe";
console.log(nombre + " " + apellido);
// mejor así
console.log(`Hola ${nombre} ${apellido}`);
14.7 Operador de asignación si izquierda nula ??
Existe el operador nullish coalescing assignment que coge el dato de la derecha únicamente cuando el de la izquierda vale null o undefined.
Nullish coalescing assignment
const a = { duration: 50 };
a.speed ??= 25;
console.log(a.speed);
// Salida esperada: 25
a.duration ??= 10;
console.log(a.duration);
// Salida esperada: 50
Este operador evita muchos errores relacionados con valores inexistentes.
15 Entrada y salida de datos
Todo programa necesita comunicarse con el usuario, y normalmente la comunicación consiste en:
- Mostrar información.
- Solicitar datos.
- Presentar mensajes de error.
- Confirmar una acción.
En JavaScript existen diferentes mecanismos para realizar estas tareas, y aunque en un principio debería ser en un formato Web, para comenzar vamos a ver otras maneras y así también poder usarlas para debuggear.
15.1 Salida mediante la consola
La forma más habitual durante el desarrollo consiste en utilizar la consola del navegador. La función más utilizada y la que se ha estado utilizando hasta ahora. Aparte del formato log podemos utilizar otros métodos detallados a continuación:
Salida mediante consola
console.log("Hola mundo");
console.info("Información");
console.warn("Advertencia");
console.error("Error");
Para los dos últimos métodos, algunos navegadores los muestran utilizando diferentes colores o iconos.
15.2 Solicitar datos mediante prompt()
La función prompt() muestra una ventana solicitando información al usuario, que deberá escribir el valor y pulsar el botón Aceptar. El valor deberá ser guardado en una variable
Solicitar datos
const nombre = prompt("Introduce tu nombre");
prompt() siempre devuelve una cadena de texto.
Esta función hoy en día no se usa ya que tiene varios inconvenientes y su función se puede obtener a través de formularios:
- Interrumpen la ejecución del programa.
- Bloquean la interacción con la página.
- Su apariencia depende del navegador.
- No pueden personalizarse.
Pide con prompt() el nombre y apellido y usa la consola para mostrar los datos usando template literals
15.3 Mostrar mensajes mediante alert()
La función alert() muestra una ventana con un mensaje. El programa permanece detenido hasta que el usuario pulse el botón de aceptación. Hoy en día apenas se usa por las mismas razones que prompt().
Método alert
alert("Bienvenido");
15.4 Solicitar confirmación mediante confirm()
La función confirm() muestra una pregunta con dos botones para Aceptar o Cancelar, que nos devolverá un valor booleano.
Método alert
const borrar = confirm("¿Desea eliminar el registro?");
De nuevo, una función apenas usada ya que es habitual usar modals
15.5 Mostrar información en la página
Además de la consola, JavaScript puede modificar el contenido de una página web. Profundizaremos más adelante, pero sirva el siguiente ejemplo como un adelanto.
index.html
<p id="mensaje"></p>
Código js/app.js
document.getElementById("mensaje").textContent = "Hola!";
16 Estructuras de control
Hasta ahora hemos aprendido a declarar variables, almacenar información y realizar operaciones con ellas. Todos lo hemos visto hasta ahora ejecutan las instrucciones de manera secuencial, una después de otra. Un programa debe ser capaz de tomar decisiones, repetir tareas o ejecutar unas instrucciones u otras dependiendo de la información introducida por el usuario.
Las estructuras de control permiten modificar el flujo normal de ejecución de un programa, y gracias a ellas podremos crear aplicaciones complejas y dinámicas. Tal como sabéis del curso pasado las estructuras de control se diferencian en:
- Estructuras secuenciales: se ejecutan una detrás de otra.
- Estructuras condicionales: ejecuta instrucciones dependiendo de una condición.
- Estructuras repetitivas: permite ejecutar un bloque varias veces.
16.1 Estructuras condicionales
Tal como existe en otros lenguajes de programación la estructura condicional if es la más sencilla y ejecutará el contenido si se cumple una condición. Si esa condición no se cumple y existe el bloque else se ejecutará este segundo bloque.
Condicional simple
const edad = 20;
if (edad >= 18) {
console.log("Mayor");
}
Condicional con else
const edad = 20;
if (edad >= 18) {
console.log("Mayor");
} else {
console.log("Menor");
}
Pide con prompt() la edad y usa un condicional para saber si es: mayor o menor.
En la condición se pueden usar los operadores de comparación vistos previamente y si queremos concatenar más de una condición, los operadores lógicos.
Operador lógico
const edad = 20;
if (edad >= 18 && edad <=65) {
console.log("Mayor");
} else {
console.log("Menor o jubilado");
}
Condicional con else if
let edad = 20;
if (edad >= 65) {
console.log("Jubilado");
} else if (edad >= 18){
console.log("Mayor");
} else {
console.log("Menor");
}
Tal como se puede ver, también se puede utilizar else if si queremos comprobar una nueva condición que no se valide en la anterior.
Pide con prompt() la edad y usa un condicional para saber si es: menor, mayor o jubilado.
16.1.1 Operador ternario
Si queremos escribir una estructura sencilla if..else podemos hacer uso del operador ternario en una única línea de la siguiente manera:
Operador ternario
const edad = 20;
const mensaje = edad >= 18 ? "Mayor" : "Menor";
16.1.2 Estructura switch
Si queremos comprobar varias condiciones, para no tener que anidar if else podemos hacer uso de switch:
Usando switch
switch (action) {
case "draw":
console.log("draw");
break;
case "eat":
console.log("eat");
break;
default:
console.log("default");
}
Es importante añadir la sentencia break al final de cada bloque, porque si no se seguirá ejecutando en “cascada” el resto de niveles inferiores. El bloque default se ejecutará si no coincide con ninguno de los bloques anteriores.
Usa el condicional switch usando y sin usar break.
17 Bucles
Para realizar sentencias repetitivas y la ejecución repetida de bloques existen las estructuras repetitivas, también conocidas como bucles.
Un bucle permite ejecutar un bloque de instrucciones varias veces mientras se cumpla una determinada condición. Tal como pasa en otros lenguajes de programación, en JavaScript hay varios tipos de bucles:
17.1 El bucle while
El bucle while ejecuta un bloque mientras la condición sea verdadera. La condición se evalúa antes de cada iteración, y por tanto, si la condición es falsa, no se realizará el bucle.
Bucle while
let contador = 1;
while (contador <= 5) {
console.log(contador);
contador++;
}
Bucle infinito
let contador = 1;
while (contador <= 5) {
console.log(contador);
}
Es importante tener en cuenta la condición para evitar bucles infinitos.
17.2 El bucle do…while
En este caso la condición se evalúa después de ejecutar el bloque.
Bucle do…while
let contador = 1;
do {
console.log(contador);
contador++;
} while (contador <= 5);
17.3 El bucle for
Es el bucle más utilizado cuando conocemos de antemano el número de repeticiones.
Bucle for
for (let i = 1; i <= 5; i++) {
console.log(i);
}
En este caso en la sentencia se diferencian tres partes:
- Inicialización: let i = 0
- Condición: i < 10
- Actualización: i++
Crea un ejemplo de cada uno de los bucles vistos.
17.4 El bucle for…of
JavaScript moderno incorpora el bucle for…of, diseñado para recorrer objetos iterables (Aray, Map, Set…).
Es la forma recomendada para recorrer arrays.
Bucle for…of
const colores = ["Rojo", "Verde", "Azul"];
for (const color of colores) {
console.log(color);
}
17.5 El bucle for…in
Con el bucle for…in se puede recorrer las propiedades de un objeto.
Bucle for…in
const alumno = {
nombre: "Bob",
edad: 20
};
for (const propiedad in alumno) {
console.log(propiedad);
}
También puede utilizarse sobre arrays, aunque no es recomendable. Para recorrer arrays utilizaremos siempre for…of.
17.6 Resumen de bucles
En la siguiente tabla se puede ver un resumen de los tipos de bucles y para qué deberíamos usarlos.
| Situación | Bucle recomendado |
|---|---|
| Número conocido de repeticiones | for |
| Repetir mientras se cumpla una condición | while |
| Ejecutar al menos una vez | do...while |
| Recorrer arrays | for...of |
| Recorrer propiedades de objetos | for...in |
17.7 Las sentencias break y continue
En ocasiones es necesario modificar el comportamiento normal de un bucle, y para ello están las siguiente instrucciones:
- break: finaliza inmediatamente el bucle.
- continue: salta la iteración actual y continúa con la siguiente.
Sentencia “break”
const numeros = [4, 8, 15, 23];
for (const numero of numeros) {
if (numero === 15) {
console.log("Encontrado");
break;
}
}
Sentencia “continue”
for (let i = 1; i <= 5; i++) {
if (i === 3) {
continue;
}
console.log(i);
}
Crea un ejemplo de cada uno de los bucles vistos.
18 Gestión de errores básica
JavaScript permite detectar y gestionar errores para evitar que la aplicación finalice de forma inesperada.
18.1 Estructura try…catch
La estructura de gestión de excepciones que está presente en muchos lenguajes de programación es try…catch, y JavaScript no iba a ser la excepción.
Si realizamos una sentencia que es propensa a generar un error, el programa puede pararse. Si esa sentencia se realiza dentro de un try…catch podemos controlar la gestión del error.
Ejemplo de gestión de errores
try {
console.log(variableInexistente);
}
catch (error) {
console.log("Error: "+error);
}
18.1.1 El objeto error
El catch recibe un objeto con información sobre el error, que cuenta con dos atributos:
- name: el nombre del error.
- message: el mensaje de error.
18.1.2 Bloque finally
Junto al bloque try…catch podemos añadir un bloque opcional llamado finally que se ejecutará siempre, haya o no error.
Ejemplo de gestión de errores
try {
console.log("Inicio");
}
catch (error) {
console.log("Error: "+error);
}
finally {
console.log("Fin");
}
18.2 Generar errores con throw
También es posible producir errores de forma intencionada con la sentencia throw.
Ejemplo de crear errores
const edad = -5;
if (edad < 0) {
throw new Error("La edad no puede ser negativa.");
}
19 Funciones
Hasta este momento hemos escrito programas formados por una secuencia de instrucciones. A medida que los programas crecen, es habitual encontrarse con bloques de código que realizan siempre la misma tarea. Copiar y pegar ese código varias veces hace que el programa sea más difícil de mantener y aumenta la probabilidad de cometer errores.
Las funciones permiten agrupar un conjunto de instrucciones bajo un nombre para poder ejecutarlas siempre que sea necesario. Las funciones constituyen uno de los elementos más importantes de cualquier lenguaje de programación.
19.1 Declaración y uso de funciones
La forma clásica de crear una función consiste en utilizar la palabra reservada function. Para llamar a la función usaremos la palabra reservada junto con ().
Declarar función
function saludar() {
console.log("Hola.");
}
Llamar a función
saludar();
saludar();
Las funciones declaradas mediante function pueden utilizarse incluso antes de aparecer en el código.
19.2 Parámetros
En muchas ocasiones una función debe trabajar con datos diferentes cada vez que se ejecuta y para ello se utilizan los parámetros. Un parámetro es una variable que recibe un valor cuando la función es llamada. Una función puede recibir varios parámetros.
Declarar función
function saludar(nombre) {
console.log(`Hola ${nombre}`);
}
Llamar a función
saludar("Ann");
saludar("Bob");
Aunque es habitual llamar como sinónimos “parámetro” o “argumento”, existe una pequeña diferencia:
- Parámetro: variable definida en la función.
- Argumento: valor que se pasa al llamar a la función.
19.2.1 Valor por defecto
Desde ECMAScript 2015 es posible asignar un valor por defecto a los parámetros. De esta manera podemos llamar a la función añadiendo el parámetro o no.
Declarar función
function saludar(nombre="Usuario") {
console.log(`Hola ${nombre}`);
}
Llamar a función
saludar();
saludar("Alice");
Crea un ejemplo de función con valor por defecto.
19.2.2 Paso de argumentos
En otros lenguajes de programación, al declarar una función tenemos que llamarla con el mismo número de parámetros. También suelen permitir usar el mismo identificador de función con distintos parámetros, lo que se llama sobrecarga de funciones. Esto no sucede en JavaScript, ya que no obliga a proporcionar exactamente el mismo número de argumentos que parámetros. Esto puede considerarse una ventaja pero también hay que tener cuidado.
Declarar función
function sumar(a, b) {
console.log(a+b);
}
Llamar a función
sumar(4);
El segundo parámetro, al no haberle pasado un argumento, tendrá el valor undefined. Por este motivo conviene utilizar parámetros por defecto cuando sea necesario o tenerlo en cuenta al crear la función.
Crea un ejemplo de función con parámetros.
19.3 Valor de retorno
Muchas funciones no solo realizan una tarea, sino que además producen un resultado. Para ello se utiliza la palabra reservada return para devolver el resultado.
Declarar función
function sumar(a, b) {
return a + b;
}
Llamar a función
let resultado = sumar(4,6);
console.log(resultado);
Si una función no tiene return el valor que devuelve por defecto es undefined.
Cuando se ejecuta return la función termina. Si no hay return, por defecto devuelve undefined.
Crea un ejemplo de función con retorno.
19.4 Funciones anónimas y funciones flecha (arrow Functions)
JavaScript moderno incorpora una sintaxis para crear funciones anónimas, más compacta para definir funciones que carecen de nombre/identificador. Otra manera de crear funciones anónimas son las conocidas como “funciones flecha” (arrow functions).
Las funciones anónimas son usadas cuando no es necesario reutilizar su código, para asignar valores directamente a una variable, para asignar como argumento a otra función o para ser devueltas desde una función.
A continuación distintos ejemplos de funciones anónimas y la sintaxis que se puede utilizar.
Función anónima
const m = function(x, y){
return x * y;
};
Función flecha
const m = (x, y) => {
return x * y;
}
Función flecha
const m = (x, y) => x * y;
Función flecha
const m = () => "Hola!";
Crea distintos ejemplos de funciones anónimas y flecha.
Objetos
20 Objetos
Hasta ahora hemos trabajado con variables capaces de almacenar un único dato, pero como ya sabemos, en una aplicación real normalmente usamos entidades y datos más complejos. Esto hace que usar sólo variables independientes sea ineficiente y poco real.
En JavaScript podemos usar objetos, que son una estructura que permite agrupar información relacionada. Cada dato dentro de un objeto es una propiedad, que se puede declarar con o sin comillas (normalmente sin comillas). De los objetos de JavaScript se deriva el formato JSON (JavaScript Object Notation), ampliamente utilizado hoy en día en internet, como alternativa a XML.
Crear objeto
const alumno = {
nombre: "Alice",
edad: 20,
direccion: {
ciudad: "Bilbao",
calle: "una",
}
};
Crear objeto
const alumno = {
"nombre": "Bob",
"edad": 20,
"direccion": {
"ciudad": "BI",
"calle": "una",
}
};
Crear objeto
const nombre = "Bob";
const edad = 30;
const alumno = {
nombre,
edad,
//...
};
En el ejemplo anterior tenemos el objeto alumno con tres propiedades y cada uno con su valor. Los objetos pueden modificarse incluso aunque hayan sido declarados con const ya que se está cambiando el contenido, pero no la referencia (que es lo que protege const).
20.1 Propiedades
Tal como se ha mencionado, un objeto contiene una colección de propiedades, y sobre ellas tenemos la posibilidad de realizar distintas operaciones que vamos a detallar a continuación:
20.1.1 Acceder y modificar propiedades
Una vez creado un objeto, es posible consultar el valor de sus propiedades, modificarlas, añadir otras nuevas o eliminarlas. JavaScript proporciona varias formas de acceder a la información almacenada dentro de un objeto.
Para acceder a las propiedades podemos hacerlo de dos maneras:
Acceder a propiedades
console.log(alumno.nombre);
console.log(alumno.edad);
console.log(alumno.direccion.calle);
Acceder a propiedades
console.log(alumno["nombre"]);
console.log(alumno["direccion"]["calle"]);
//evitad mezclar
console.log(alumno["direccion"].calle);
El uso de corchetes suele ser más habitual cuando el nombre de la propiedad lo tenemos en una variable.
Acceder a propiedades
let propiedad = "direccion";
console.log(alumno[propiedad])
Modificar propiedades
alumno.nombre = "Bob";
alumno.curso = "2º DAW";
Para modificar la propiedad usaríamos la misma forma tal como se ve en el código de arriba. Y si queremos añadir propiedades nuevas, igual.
20.1.2 Eliminar propiedades
En caso de querer borrar una propiedad usaremos el operador delete. Por defecto siempre devuelve true aunque hay excepciones.
Eliminar propiedades
delete alumno.curso;
20.1.3 Acceder a propiedad inexistente
Es posible que en algún caso intentemos acceder a una propiedad no definida, y por tanto inexistente. El resultado será undefined, por lo que no dará error.
20.1.4 Acceso a objeto inexistente con ?.
Si un objeto puede no existir, si intentásemos acceder a él o a una propiedad del mismo, obtendremos un error. Para evitar ese error, podemos utilizar el operador ?. (llamado optional chaining).
Acceder objeto inexistente
const profesor = null;
console.log(profesor?.nombre); // undefined
console.log(profesor.nombre); // error
20.1.5 Comprobar si existe una propiedad
Una forma sencilla consiste en utilizar el operador in. Al utilizar este operador tendremos un tipo booleano, por lo que si la propiedad existe tendremos un true.
Comprobar si existe propiedad
if ("edad" in alumno) {
console.log("tiene edad")
} else {
console.log("no tiene edad")
}
20.1.6 Recorrido de propiedades
En muchas ocasiones es necesario procesar todas las propiedades de un objeto. JavaScript proporciona distintos mecanismos para recorrerlas.
20.1.6.1 Utilizando for…in
Ya hemos visto previamente que podemos hacer uso del bucle La forma más sencilla consiste en utilizar un bucle for...in. En este caso únicamente obtenemos el nombre de las propiedades.
Recorrer con for…in
const alumno = {
nombre: "Alice",
edad: 20,
ciudad: "Bilbao"
};
for (const propiedad in alumno) {
console.log(propiedad);
}
Crea un objeto, añade propiedad, borra y recorre todas las propiedades.
20.1.6.2 Obtener todas las propiedades con Object.keys()
La función Object.keys() devuelve un array con todas las propiedades del objeto.
Recorrer con Object.keys()
const propiedades = Object.keys(alumno);
console.log(propiedades);
// recorrer propiedades
for (const propiedad of Object.keys(alumno)) {
console.log(propiedad);
}
20.1.6.3 Obtener sólo los valores de las propiedades con Object.values()
Si queremos sólo los valores, podemos utilizar la función Object.values().
Obtener sólo los valores
console.log(Object.values(alumno));
20.1.6.4 Obtener propiedad y valor con Object.entries()
Devuelve un array formado por pares en el formato [[clave,valor],…].
Obtener sólo los valores
console.log(Object.values(alumno));
// recorrer propiedad/valor
for (const [propiedad, valor] of Object.entries(alumno)) {
console.log(propiedad, valor);
}
Usa Object.entries() para recorrer un objeto.
20.1.6.5 ¿Qué método utilizar?
A modo de resumen sirva la siguiente tabla.
| Método | Devuelve |
|---|---|
| for…in | Propiedades del objeto. |
| Object.keys() | Array con las propiedades. |
| Object.values() | Array con los valores. |
| Object.entries() | Array con pares propiedad-valor. |
En JavaScript moderno es muy frecuente utilizar Object.entries() junto con for…of, ya que proporciona una sintaxis clara y fácil de leer, pero dependerá de qué necesitamos.
Usa el método para recorrer objetos que mejor se adecúe en cada momento.
20.2 Métodos
Los objetos también puede contener funciones, que reciben el nombre de métodos. Los métodos se pueden declarar de dos maneras tal como podemos ver a continuación:
Método
const alumno = {
nombre: "Carol",
saludar: function () {
console.log("Hola.");
}
};
alumno.saludar();
Método
const alumno = {
nombre: "David",
saludar (saludo) {
console.log(`Hola ${saludo}`);
}
};
alumno.saludar("tu");
El valor “this”
Dentro de un método podemos acceder al propio objeto utilizando la palabra reservada this.
Uso de “this”
const alumno = {
nombre: "Eve",
saludar() {
console.log(`Hola ${this.nombre}`);
}
};
alumno.saludar();
“this” no funciona arrow-functions
const alumno = {
nombre: "Frank",
saludar: => () {
console.log(`Hola ${this.nombre}`);
//undefined
}
};
alumno.saludar();
Aunque técnicamente es posible utilizar funciones flecha como métodos, normalmente no es recomendable. Al utilizar funciones flecha no crean su propio this, por lo que puede producir problemas con los resultados.
Es mejor evitar las arrow functions como método.
Crea un método dentro de un objeto y haz uso de él. Prueba en otro método un arrow function con this. ¿Qué pasa?
20.3 Copia de objetos
En muchas ocasiones necesitamos crear una copia de un objeto para modificarla sin alterar el original. Sin embargo, copiar un objeto en JavaScript no siempre produce el resultado esperado. Para comprender este comportamiento es necesario recordar que las variables que almacenan objetos guardan una referencia al objeto, no el objeto en sí.
Imaginemos el siguiente ejemplo en el que creamos un objeto y creamos otra variable asignando el objeto:
Asignación de objetos
const alumno1 = {
nombre: "Grace",
edad: 20
};
const alumno2 = alumno1;
Añade o modifica en alumno2 una propiedad. ¿Qué pasa con alumno1?
Ambas variables hacen referencia al mismo objeto:
Esto hace que aunque modifiquemos alumno2 el cambio también se ve reflejado en alumno1. Es más, si realizamos la comparación con === nos devuelve que es true, porque son el mismo objeto.
Comparación de objetos
const alumno1 = {
nombre: "Heidi",
edad: 20
};
const alumno2 = alumno1;
console.log(alumno1 === alumno2);
const alumno3 = {
nombre: "Heidi",
edad: 20
};
console.log(alumno1 === alumno3);
Realiza el ejercicio anterior e interioriza lo que sucede.
20.3.1 Copia superficial
Para realizar una copia real del objeto debemos hacer uso del operador Spread …, aunque también se puede utilizar Object.assign().
Copia superficial
const alumno1 = {
nombre: "Ivan",
edad: 20,
direccion: {
ciudad: "Bilbao"
}
};
const alumno2 = {...alumno1};
console.log(alumno1 === alumno2);
Copia superficial
const alumno1 = {
nombre: "Alice",
edad: 20,
direccion: {
ciudad: "Bilbao"
}
};
const alumno2 = Object.assign({}, alumno1);
console.log(alumno1 === alumno2);
Cambia la edad y la ciudad en alumno1. ¿Qué sucede en alumno2?
Cuidado que sólo son copias superficiales. Los objetos anidados siguen siendo el mismo objeto.
20.3.2 Copia profunda
Cuando es necesario copiar completamente un objeto, incluidos todos los objetos anidados, hablamos de una copia profunda (deep copy). En estos casos tenemos que hacer uso de structuredClone().
Copia profunda
const alumno1 = {
nombre: "Bob",
edad: 20,
direccion: {
ciudad: "Bilbao"
}
};
const alumno2 = structuredClone(alumno1);
Cambia la edad y la ciudad en alumno1. ¿Qué sucede en alumno2?
20.4 Combinar objetos
El operador spread también puede utilizarse para combinar objetos.
Combinar objetos
const persona = {
nombre: "Carol"
};
const datos = {
...persona,
edad: 20,
ciudad: "Bilbao"
};
console.log(datos);
20.5 Sobrescribir propiedades
Si una propiedad aparece varias veces, prevalece la última.
Sobrescribir propiedades
const alumno = {
nombre: "Dan",
edad: 20
};
const copia = {
...alumno,
edad: 21
};
21 Arrays
Hasta ahora hemos visto cómo los objetos permiten agrupar información relacionada, sin embargo, en muchas ocasiones necesitamos almacenar varios elementos. Para ello, al igual que en otros lenguajes, JavaScript incorpora los arrays.
A diferencia de otros lenguajes, JavaScript permite crear arrays de distintos tipos de datos. El ejemplo más claro es un array de objetos son los datos que obtenemos de una API en formato JSON.
Acceder simple
const datos = [10, 20, "treinta"];
const llenar = [];
Array objetos
const alumnos = [
{
nombre: "Ann",
edad: 20
},
{
nombre: "Bob",
edad: 21
}
];
Aparte del “array tradicional”, también existen otras colecciones indexadas.
21.1 Acceder y modificar elementos
Para acceder y modificar elementos de un array debemos hacerlo a través de su índice. En JavaScript los índices empiezan en 0.
Acceder a un índice
console.log(datos[0]);
Modificar un índice
datos[2] = 30;
21.2 Longitud del array
Para poder saber cuántos elementos tiene el array tenemos la propiedad length. Por lo tanto, si queremos acceder al último elemento, tendremos que restarle 1.
Longitud del array
console.log(datos.length);
Último elemento
datos[datos.length-1];
21.3 Recorrer un array
La forma clásica consiste en utilizar un bucle for, aunque actualmente se hace uso de for…of.
Recorrer array
for (let i=0; i<datos.length; i++) {
console.log(datos[i]);
}
Recorrer array
for (const dato of datos) {
console.log(dato);
}
Crea un array, modifica algún elemento, comprueba su longitud y recórrelo.
21.4 Métodos de arrays
JavaScript incorpora numerosos métodos para trabajar con arrays. Muchos de ellos modifican el contenido del array y otros devuelven uno nuevo. A continuación veremos los más utilizados, pero es interesante ver la documentación de arrays para ver todos.
Métodos array
const numeros = [10, 20, 30, 40, 50];
// añadir elemento al final
numeros.push(60);
// eliminar último elemento
numeros.pop();
// añadir al inicio
numeros.unshift(5);
// eliminar el primero
numeros.shift();
// obtener una parte del array
const nums = numeros.slice(1, 3);
Métodos array
const colores = ["Rojo","Verde","Azul"];
// buscar un elemento
console.log(colores.includes("Verde"));
// obtener la posición
console.log(colores.indexOf("Azul"));
// concatenar arrays
const nuevo = numeros.concat(colores);
const letras = ["C", "A", "B"];
// ordenar
letras.sort();
// invertir
letras.reverse();
// convertir en cadena
console.log(colores.join(", "));
En la siguiente tabla un resumen de los métodos vistos:
| Método | Descripción |
|---|---|
| push() | Añade al final |
| pop() | Elimina el último |
| unshift() | Añade al principio |
| shift() | Elimina el primero |
| includes() | Comprueba si existe |
| indexOf() | Devuelve la posición |
| concat() | Une arrays |
| slice() | Obtiene una copia parcial |
| splice() | Inserta o elimina elementos |
| sort() | Ordena |
| reverse() | Invierte el orden |
| join() | Convierte en texto |
21.5 Copia de arrays
Al igual que pasaba con los objetos, si tenemos un array creado, al asignarlo a otra variable no estamos realizando una copia, sino una referencia al dato. Es importante tener esto en cuenta, por lo que recuerda lo visto en copia profunda de los objetos para hacerlo con arrays.
Usa los métodos anteriores y copia arrays como los objetos.
Programación Orientada a Objetos
22 Programación orientada a objetos
Hasta ahora hemos estado usando objetos literales, y aunque este tipo de objetos resulta suficiente para muchas aplicaciones, es posible que el desarrollo que tengamos que hacer se plantee desde el inicio como objetos con la misma estructura.
Por ejemplo, una aplicación de gestión de un instituto puede manejar cientos de alumnos, y todos ellos van a contar con las mismas características, van a tener la misma estructura. La Programación Orientada a Objetos (POO) permite definir un modelo común para crear tantos objetos como sea necesario.
JavaScript incorpora desde ECMAScript 2015 una sintaxis basada en clases, muy similar a la utilizada por otros lenguajes modernos. Aunque internamente JavaScript continúa utilizando prototipos, la sintaxis mediante clases facilita el aprendizaje y es la forma habitual de programar en la actualidad.
22.1 Resumen de conceptos en POO
Aunque ya se ha programado con orientación a objetos, a continuación se resume los conceptos más importantes:
- Clase: describe cómo serán los objetos.
- Objeto: es una instancia concreta creada a partir de esa clase.
- Propiedades/Atributos: representan el estado del objeto (nombre, edad, …).
- Métodos: representan el comportamiento, o funciones, que puede realizar el objeto.
Tradicionalmente la Programación Orientada a Objetos se basa en cuatro conceptos fundamentales, que ya nos deberían sonar, o al menos, saber utilizar:
- Abstracción: consiste en representar un sistema enfocándose en sus características esenciales
- Encapsulación: es la agrupación de datos con los métodos que operan en esos datos.
- Herencia: gracias a la herencia se pueden crear nuevas clases partiendo de una ya preexistente.
- Polimorfismo: se refiere a la propiedad por la que es posible enviar mensajes sintácticamente iguales a objetos de tipos distintos.
23 Crear una clase
Para crear una clase se usa la palabra reservada class.Su sintaxis general es la que se verá a continuación, y el nombre de la clase, por convenio, suele comenzar con letra mayúscula.
A diferencia de otros lenguajes de programación, no hace falta indicar los campos públicos ya que se inicializan a través del constructor, que es un método especial que se ejecuta automáticamente cuando se crea un nuevo objeto. Cada clase únicamente puede tener un constructor.
Cada clase únicamente puede tener un constructor.
Nueva clase
class Alumno {
constructor(nombre, edad) {
this.nombre = nombre;
this.edad = edad;
}
saludar() {
console.log(`Hola ${this.nombre}`);
}
}
Crear objetos
const alumno1 = new Alumno("Alice",20);
const alumno2 = new Alumno("Bob",25);
alumno1.saludar();
alumno2.saludar();
Tal como se puede ver, se ha creado también un método saludar que se puede llamar después de crear un objeto de la clase.
23.1 Encapsulación
Uno de los principios fundamentales de la Programación Orientada a Objetos es la encapsulación, que consiste en proteger el estado interno de un objeto y controlar la forma en que puede accederse a él.
En lugar de permitir que cualquier parte del programa modifique libremente los datos, el propio objeto decide cómo deben utilizarse.
Nueva clase
class Cuenta {
constructor(saldo) {
this.saldo = saldo;
}
setSaldo(saldo){
this.saldo = saldo;
}
}
Crear objetos
const cuenta = new Cuenta(1000);
// nada impide hacer
cuenta.saldo = -5000;
// debería ser
cuenta.setSaldo(1500);
Desde el punto de vista de la programación, acceder a un campo y modificarlo es posible, pero puede afectar al comportamiento del mismo. Es por eso que para evitar problemas deberíamos hacer uso de la encapsulación y crear métodos que modifiquen las propiedades.
Crea una clase con al menos dos atributos y un método.
23.2 Campos y métodos privados
Aunque pueda parecer extraño, JavaScript incorporó los elementos y métodos privados en julio del 2021. Para declarar propiedades y métodos privados se hace usando el símbolo #.
Una propiedad y un método privado sólo se podrá acceder desde la propia clase, tal como sucede en otros lenguajes que cuentan con programación orientada a objetos.
Nueva clase
class Cuenta {
#saldo;
constructor(saldo) {
this.#saldo = saldo;
}
#privado() {
console.log("Priv");
}
getSaldo() {
this.#privado();
return this.#saldo;
}
}
Crear objetos
const cuenta = new Cuenta(1000);
// obtener dato
console.log(cuenta.getSaldo());
//esto dará error
console.log(cuenta.#privado());
//esto dará error
console.log(cuenta.#saldo);
Tal como se puede ver, para obtener acceso a la propiedad privada debemos hacer uso de un método creado a tal efecto getSaldo(), que es público. Lo mismo si queremos modificar la propiedad, se debe hacer a través de un método.
Es importante matizar que en navegadores Chrome desde la consola de desarrollador es posible acceder al método privado, por lo que no dará error.
En navegadores Chrome desde la consola de desarrollador es posible acceder al método privado.
Crea una clase con un atributo y un método privado.
23.3 Getters y setters
Aunque anteriormente hemos visto cómo acceder a una propiedad, hemos utilizado el estilo clásico de otros lenguajes como Java. En cambio, JavaScript incorpora una sintaxis especial denominada getter y setter.
Un getter permite consultar una propiedad como si fuese un atributo, mientras que el setter permite modificar una propiedad.
Setter y getter
class Cuenta {
#saldo;
constructor(saldo) {
this.#saldo = saldo;
}
get saldo() {
return this.#saldo;
}
set saldo(valor) {
if (valor >= 0) {
this.#saldo = valor;
}
}
}
Cómo se usa
console.log(cuenta.saldo);
cuenta.saldo = 500;
console.log(cuenta.saldo);
Coge una clase de los ejercicios anteriores y modifica para usar getters y setters.
24 Herencia
La herencia consiste en crear una nueva clase a partir de otra ya existente. La nueva clase hereda las propiedades y métodos de la clase original. En JavaScript se utiliza la palabra reservada extends para realizar la herencia.
Cuando una clase hija define su propio constructor, debe llamar al constructor de la clase padre mediante super(). Por supuesto, esta nueva clase puede contar con nuevos atributos y métodos que hagan nuevas funciones.
Clase
class Persona {
constructor(nombre, edad) {
this.nombre = nombre;
this.edad = edad;
}
saludar() {
console.log(`Hola,
soy ${this.nombre}`);
}
}
Clase heredada
class Alumno extends Persona {
constructor(nombre, edad, curso) {
super(nombre, edad);
this.curso = curso;
}
}
const alumno = new Alumno(
"Ana",20,"2º DAW"
);
alumno.saludar();
Crea una clase, otra que herede de la primera y que tenga un atributo y un método nuevo.
25 Polimorfismo
La palabra polimorfismo significa literalmente muchas formas. En Programación Orientada a Objetos indica que distintos objetos pueden responder de forma diferente al mismo método.
Teniendo en cuenta la clase “Persona” del ejemplo anterior, a continuación se han creado dos nuevas clases que heredan de ella y que sobrescriben el método saludar().
Polimorfismo
class Alumno extends Persona {
saludar() {
super.saludar();
console.log("Soy alumno.");
}
}
Polimorfismo
class Profesor extends Persona {
saludar() {
super.saludar();
console.log("Soy profesor.");
}
}
Nótese que para llamar al método de la clase padre se ha usado super.saludar(), que hace referencia al método que necesitamos del padre.
Partiendo del ejemplo anterior, haz una instancia de cada clase que llame a su método saludar().
26 Propiedades y métodos estáticos
Un método estático se declara mediante la palabra reservada static y no requieren crear un objeto para ser usados. Es más, si se crea un objeto se accede a dicho método dará error.
Propiedad y método estático
class Calculadora {
static VERSION = "1.0";
static sumar(a, b) {
return a + b;
}
}
const resultado = Calculadora.sumar(5, 8);
En principio no vamos a hacer uso de este sistema, pero es importante conocerlo ya que JavaScript cuenta con distintas clases que podemos utilizar que funcionan así, como Math.
JavaScript: uso del Document Object Model
27 El DOM (Document Object Model)
Cuando un navegador carga una página web, no trabaja directamente con el código HTML. En su lugar, analiza el documento y construye una representación interna denominada Document Object Model, o simplemente DOM.
El DOM convierte todos los elementos HTML (también XML o SVG) en objetos JavaScript (que tienen sus propios atributos), que se representa mediante un árbol de nodos o DOM tree. El DOM realmente es un web API y estándar, y a través del DOM JavaScript puede:
- Leer el contenido de una página.
- Modificar contenido.
- Cambiar estilos.
- Crear/eliminar elementos.
- Responder a las acciones del usuario.
Veamos el siguiente HTML y cuál es el árbol DOM que se genera.
HTML
<!DOCTYPE html>
<html>
<head>
<title>Ejemplo</title>
</head>
<body>
<h1>Mi página</h1>
<p>Bienvenido.</p>
</body>
</html>
Una de las características más importantes del DOM es que puede modificarse en cualquier momento. Una página puede cambiar automáticamente un texto, mostrar nuevos elementos o eliminar contenido sin necesidad de recargarse. Este comportamiento es la base de las aplicaciones web modernas.
En una página web cada tecnología desempeña una función distinta.
| Tecnología | Función |
|---|---|
| HTML | Define la estructura del documento. |
| CSS | Define la presentación y el aspecto visual. |
| JavaScript | Añade comportamiento e interacción. |
El DOM constituye el puente que permite a JavaScript modificar el contenido HTML y aplicar cambios visuales mediante CSS. Las herramientas para desarrolladores de cualquier navegador permiten inspeccionar el DOM: al pulsar F12 y seleccionar la pestaña Elements (o Inspector, según el navegador), puede observarse el árbol completo generado a partir del documento HTML.
Desde “herramientas para desarrolladores” del navegador podemos ver el árbol DOM.
27.1 Estructura del árbol DOM
El DOM organiza todos los elementos mediante relaciones familiares.
Cada nodo puede tener:
- Un padre (parent): En el ejemplo anterior el padre de <h1> es <body>.
- Uno o varios hijos (children): <body> tiene dos hijos.
- Hermanos (siblings): el <h1> tiene un hermano a nivel jerárquico que es el <p>.
Comprender estas relaciones facilita enormemente la navegación por el documento. Cada elemento conoce su posición dentro del árbol, y gracias a ello es posible recorrer toda la estructura del documento. Un símil sería una estructura de ficheros, eligiendo un fichero de manera aleatoria, podemos ver si tiene hijos (si es un directorio), hermanos (otros ficheros en la misma ruta) o subir al directorio padre.
Podemos comparar el árbol DOM como un sistema de ficheros: conociendo el elemento, podemos ir a cualquier otro.
27.2 Tipos de nodos
Aunque habitualmente trabajaremos con elementos HTML, el DOM contiene distintos tipos de nodos:
- Documento.
- Elementos HTML.
- Comentarios.
- Texto.
En el siguiente ejemplo el elemento <p> es un nodo. El texto "Bienvenido" también constituye otro nodo.
HTML
<p>Bienvenido.</p>
27.2.1 El nodo raíz: document
Todo el DOM se encuentra representado mediante un objeto global llamado document, y este objeto constituye el punto de entrada para acceder a cualquier elemento de la página. Es el nodo más alto en la jerarquía, el nodo raíz, el “padre de todos”.
Dentro de este nodo raíz, hay distintas propiedades que va a tener todo HTML, entre los que se puede destacar:
- head: este nodo es de sólo lectura y devuelve el <head> del documento.
- body: representa el cuerpo del documento, donde tendremos los nodos y elementos con los que queremos interactuar.
- title: el título del documento actual, podemos obtenerlo o modificarlo.
Crea un HTML básico y obtén las propiedades explicadas de document. Cambia el title de la página.
29 Modificación del contenido
Una vez localizado un elemento del DOM, el siguiente paso consiste en modificar su contenido. JavaScript proporciona varias propiedades para realizar esta tarea:
- textContent
- innerHTML
- innerText
Aunque las tres permiten cambiar el contenido de un elemento, existen diferencias importantes entre ellas.
29.1 Propiedad textContent
La propiedad textContent permite obtener o modificar todo el texto contenido en un elemento. Supongamos el siguiente HTML.
HTML
<h1 id="ti">Hola</h1>
<p id="hola">
Ey <span>Alice</span>
</p>
JavaScript
const ti = document.querySelector("#ti");
console.log(ti.textContent);
ti.textContent = "JavaScript";
Tal como se puede ver, se puede obtener el texto del elemento y también modificarlo.
- ¿Qué pasa si cambiamos el texto del elemento hola?
- ¿Qué pasa si lo cambiamos por <b>Hola</b>?
textContent no interpreta el código HTML.
29.2 Propiedad innerHTML
innerHTML permite leer o modificar el contenido HTML de un elemento. Resulta muy cómodo para modificar un componente y añadir nuevo HTML, el problema es que puede introducir problemas de seguridad de ejecutar código JavaScript malicioso ya que interpreta el código introducido.
HTML
<h1 id="ti">Hola</h1>
<p id="hola">
Ey <span>Alice</span>
</p>
<p id="nuevo"></p>
JavaScript
const h = document.querySelector("#hola");
h.innerHTML = "<strong>Alice</strong>";
const n = document.querySelector("#nuevo");
n.innerHTML = `
<h2>Noticias</h2>
<p>bla bla</p>
`;
- Usa innerHTML para modificar dos elementos e introduce código HTML.
- Usa <img src="img.png" onload="alert('hacked!');">
- ¿Qué pasa con esta última instrucción? (tienes que usar una imagen existente).
innerHTML puede ocasionar problemas de seguridad porque interpreta el código introducido.
29.3 Propiedad innerText
innerText también trabaja con texto, pero únicamente tiene en cuenta el contenido visible. Es decir, ignora texto oculte (hidden) y código de estilo o de etiquetas <script>. En la documentación de MDN hay un ejemplo.
- Usa la documentación de MDN y el playground para ver qué hace innerText.
- Modifica para usar el ejemplo con innerHTML y textContent.
- ¿Qué diferencia hay entre ellos?
30 Atributos de los elementos
Los elementos HTML poseen atributos que describen sus características. Con JavaScript vamos a poder consultar, modificar, añadir o eliminar dichos atributos.
Desde JavaScript vamos a poder acceder a los atributos HTML de los elementos y a las propiedades del objeto del DOM. En principio el navegador mantiene sincronizadas ambas si realizamos modificaciones.
30.1 Obtener atributos
Si queremos obtener los nombres de los atributos que están seteados de un elemento podemos utilizar getAttributeNames(). Este método nos devolverá un array con los nombres, por lo que podemos recorrerlo con un bucle de los que hemos visto previamente.
Si lo que queremos es obtener el contenido de un atributo en concreto usaremos getAttribute("nombre").
Si el atributo no existe nos devolverá null.
HTML
<img src="img.png" id="foto">
<a href="https://kernel.org">
Visitar
</a>
<input type="text">
Obtener atributos
const imagen = document.querySelector("#foto");
let nombres = imagen.getAttributeNames();
for (let n of nombres){
console.log(n);
imagen.getAttribute(n);
}
//acceder directamente al id
console.log(imagen.id);
Si conocemos los atributos del objeto podemos acceder a ellos directamente a través del objeto.
30.2 Modificar atributos
Podemos modificar atributos de un elemento, o crear nuevos atributos, usaremos la función setAttribute()
Modificar atributo
const enlace = document.querySelector("a");
//modifica el atributo HTML
enlace.setAttribute("target","_blank");
//modifica el objeto DOM
enlace.href="https://www.google.com";
30.3 Crear atributos personalizados
Aunque nada nos impide a crear atributos personalizados con el nombre que queramos, las buenas prácticas y el estándar nos dice que tenemos que crearlos con el nombre empezando por data-. La documentación nos indica que de esta manera esa información se intercambiará entre el HTML y el DOM.
Crear atributo personalizado
const enlace = document.querySelector("a");
enlace.setAttribute("data-grado","DAW");
enlace.setAttribute("data-curso",2);
//obtener todos los atributos personalizados
console.log(enlace.dataset);
//acceder a un atributo personalizado concreto
console.log(enlace.dataset.grado);
console.log(enlace.dataset.curso);
Estos atributos resultan muy útiles para almacenar información relacionada con un elemento.
30.4 Eliminar atributos
Al igual que podemos crear, también podemos eliminar un atributo concreto
Eliminar atributo
enlace.removeAttribute("target");
30.5 Comprobar si existe
Para comprobar si un atributo existe podemos usar hasAttribute() que nos devolverá un booleano.
Comprobar si existe
console.log(enlace.hasAttribute("href"));
Usa las funciones que hemos visto sobre atributos.
31 Gestión de clases CSS
Las clases CSS permiten modificar el aspecto visual de un elemento. JavaScript puede añadir clases, eliminarlas o alternarlas dinámicamente, de esta manera podemos mostrar/ocultar elementos dinámicamente, resaltar información, cambiar colores, modificar la interfaz según acciones del usuario…
Para toda acción con las clases se debe utilizar la propiedad classList que contiene distintas funciones que veremos a continuación.
La propiedad classList, sin funciones, devuelve un array de las clases.
Para los siguientes ejemplos se va a utilizar el siguiente código:
HTML
<p id="mensaje">
Bienvenido
</p>
CSS
.destacado {
color: red;
font-weight: bold;
}
.italica {
font-style: italic;
}
31.1 Añadir clases
Si queremos añadir una clase a un elemento concreto se puede usar classList.add().
Añadir clase
const mensaje = document.querySelector("#mensaje");
mensaje.classList.add("destacado");
31.2 Alternar una clase
El método classList.toggle() añade la clase si el elemento no la tiene, y si la tiene la elimina.
Alternar clase
const mensaje = document.querySelector("#mensaje");
mensaje.classList.toggle("italica");
31.3 Comprobar si existe una clase
Para saber si un elemento tiene una clase podemos usar la función classList.contains(), que nos devuelve un booleano.
Añadir clase
const mensaje = document.querySelector("#mensaje");
console.log(mensaje.classList.contains("italica"));
31.4 Reemplazar una clase
Si queremos modificar una clase por otra tenemos la función classList.replace().
Añadir clase
const mensaje = document.querySelector("#mensaje");
mensaje.classList.replace(
"destacado", // reemplaza esta clase
"normal" // por esta otra
);
31.5 Añadir varias clases
Si queremos añadir varias clases en lugar de ir una a una, podemos utilizar classList.add()
Añadir clase
const mensaje = document.querySelector("#mensaje");
mensaje.classList.add(
"rojo",
"grande",
"centrado"
);
31.6 Atributo className
El atributo className tiene dos funciones:
- Nos devuelve todas las clases existentes
- Nos permite modificar todas las clases
Añadir clase
const mensaje = document.querySelector("#mensaje");
// obtener todas las clases
console.log(mensaje.className);
mensaje.className = "destacado italica";
31.7 Resumen
| Método | Función |
|---|---|
| classList | Devuelve array de clases. |
| classList.add() | Añade una clase. |
| classList.remove() | Elimina una clase. |
| classList.toggle() | Añade o elimina una clase. |
| classList.contains() | Comprueba si existe una clase. |
| classList.replace() | Sustituye una clase por otra. |
| className | Obtiene todas las clases o las reemplaza. |
Usa las funciones anteriores para modificar las clases de un elemento.
32 Gestión de elementos del DOM
Hasta ahora hemos aprendido a modificar elementos que ya existían en la página. Normalmente, en una aplicación web también vamos a querer crear nuevos elementos, insertarlos en cualquier posición o eliminarlos cuando dejan de ser necesarios. Por ejemplo:
- Añadir un nuevo elemento a una lista de tareas.
- Crear una fila en una tabla.
- Mostrar una notificación.
- Eliminar un mensaje.
- Generar una tarjeta con información obtenida desde una API.
JavaScript proporciona varios métodos para realizar estas tareas.
32.1 Crear un elemento
Para crear un elemento se utiliza la función createElement(). Supongamos que queremos crear un párrafo. El proceso cuenta con varios pasos:
- Crear el elemento: En este momento el elemento todavía no aparece en la página, ya que el elemento se ha creado y sólo existe en memoria.
- Añadir contenido: Tras crearlo, podemos añadirle contenido o modificar sus propiedades.
- Insertarlo en el documento: Para que el navegador lo muestre es necesario insertarlo en el DOM. Tendremos que pensar dónde queremos insertar el elemento, que puede ser dentro de otro elemento o en el body.
Añadir elemento
// crear elemento
const parrafo = document.createElement("p");
// añadir contenido
parrafo.textContent = "Hola mundo";
// modificar sus propiedades
parrafo.className = "importante";
// insertarlo en el documento
document.body.append(parrafo);
En este caso se ha creado un párrafo, se le ha dado contenido, se le ha añadido una clase nueva y se ha añadido al final del body.
Vamos a hacer un ejemplo más complejo, creando una estructura de elementos completamente nueva:
Generar elementos
const tarjeta = document.createElement("div");
const titulo = document.createElement("h2");
const descripcion = document.createElement("p");
titulo.textContent = "JavaScript";
descripcion.textContent = "Creado en...";
tarjeta.append(titulo);
tarjeta.append(descripcion);
document.body.append(tarjeta);
HTML generado
<body>
<!-- ... -->
<div>
<h2>JavaScript</h2>
<p>Creado en...</p>
</div>
</body>
32.1.1 ¿Qué método utilizar?
Anteriormente hemos visto el método innerHTML y ahora el método createElement(), y aunque ambas técnicas son válidas, presentan diferencias importantes.
| createElement() | innerHTML |
|---|---|
| Crea objetos del DOM. | Inserta código HTML. |
| Más seguro. | Puede introducir riesgos de seguridad. |
| Muy adecuado para contenido dinámico. | Cómodo para generar mucho HTML. |
| Permite modificar cada elemento individualmente. | Reemplaza el contenido del elemento. |
En aplicaciones modernas debe utilizarse createElement() cuando los elementos se crean dinámicamente a partir de datos.
En aplicaciones modernas debe utilizarse createElement().
32.1.2 Insertar elemento creado
Cuando creamos un elemento nuevo (o un pequeño árbol de elementos) tenemos que decidir dónde insertarlo. Para ello contamos con distintos métodos:
| Método | ¿Dónde? |
|---|---|
| append() | Inserta como último hijo. |
| prepend() | Inserta al comienzo, como primer hijo. |
| before() | Inserta antes del elemento. |
| after() | Inserta después del elemento. |
HTML
<h1>Comprar</h1>
<ul id="lista">
<li>Tomates</li>
</ul>
Javascript
const lista = document.querySelector("#lista");
const pan = document.createElement("li");
pan.textContent = "Pan";
lista.append(pan);
const arroz = document.createElement("li");
arroz.textContent = "Arroz";
lista.prepend(arroz);
const p = document.createElement("p");
p.textContent = "Hay que comprar:"
lista.before(p);
const fin = document.createElement("p");
fin.textContent = "No te olvides de nada!"
lista.after(fin);
32.1.3 Mover un elemento
Un detalle importante del DOM es que un elemento solo puede existir una vez. Si insertamos un elemento en otra posición, el navegador no crea una copia, simplemente lo mueve.
Javascript
const lista = document.querySelector("#lista");
const elemento = document.createElement("li");
elemento.textContent = "Pan";
lista.prepend(elemento);
elemento.textContent = "Arroz";
lista.append(elemento);
Después de la segunda instrucción, el elemento únicamente existirá al final de la lista.
32.2 Eliminar elemento
Si queremos eliminar un elemento es una tarea sencilla. Una vez elegido el elemento concreto usamos la función remove() y desaparecerá del DOM.
Javascript
const lista = document.querySelector("#lista");
lista.remove();
32.3 Reemplazar elemento
El contenido de un elemento también puede ser sustituido por otro.
HTML
<p id="mensaje">
Texto antiguo
</p>
Javascript
const nuevo = document.createElement("h2");
nuevo.textContent = "Nuevo mensaje";
const antiguo = document.querySelector("#mensaje");
antiguo.replaceWith(nuevo);
32.4 Vaciar elemento
Si sólo queremos eliminar el contenido dentro de un elemento (no eliminar el elemento) podemos hacer lo siguiente:
Javascript
lista.textContent = "";
// alternativa
lista.innerHTML = "";
Recorre un array de ingredientes y añade los nuevos ingredientes creando elementos de tipo <li> insertando al inicio y luego al final de una lista.
32.5 Optimización de inserciones con DocumentFragment
En los ejemplos anteriores hemos creado e insertado elementos directamente en el DOM. Esos ejemplos de código funcionan correctamente y son perfectamente válido, pero cuando el número de elementos es muy grande, puede resultar poco eficiente.
Cada vez que se inserta un nuevo elemento, el navegador debe actualizar el árbol DOM y, en muchas ocasiones, volver a calcular el diseño de la página.
Cuando se realizan cientos o miles de inserciones consecutivas, este proceso puede afectar al rendimiento de la aplicación. Para solucionar este problema, JavaScript incorpora el objeto DocumentFragment().
Un DocumentFragment() es un contenedor temporal que no tiene padre, que permite construir un objeto más ligero de tipo Document completamente en memoria. Los elementos se crean y se añaden al fragmento, pero todavía no forman parte del documento DOM principal. Una vez terminado, todo el contenido del fragmento se inserta en el DOM mediante una única operación.
32.5.1 Crear un fragmento
Un fragmento se crea mediante createDocumentFragment(). A partir de ese momento puede utilizarse como si fuera cualquier otro elemento del DOM.
Crear fragmento
const fragmento = document.createDocumentFragment();
32.5.2 Añadir elementos
Vamos a suponer que tenemos una lista vacía y queremos añadir varios elementos en ella. Anteriormente hemos visto cómo añadir uno a uno. En este caso lo que
Añadir elementos al fragmento
const lenguajes = [
"HTML",
"CSS",
"JavaScript",
"TypeScript",
"Vue"
];
for (const lenguaje of lenguajes) {
const elemento = document.createElement("li");
elemento.textContent = lenguaje;
fragmento.append(elemento);
}
En este momento la página continúa exactamente igual. Los elementos todavía no se muestran, ya que no se han insertado en el DOM.
32.5.3 Insertar fragmento
Una vez hemos terminado de añadir todos los elementos al fragmento debemos añadirlo al DOM
HTML
<h1>Lenguajes</h1>
<ul id="lista">
</ul>
Javascript
const lista = document.querySelector("#lista");
lista.append(fragmento);
Una vez hecho, el usuario ve el resultado final. Un detalle importante es que el fragmento no se inserta como un nodo más. El fragmento queda vacío y puede volver a reutilizarse si fuera necesario.
32.5.4 ¿Por qué es más eficiente?
Supongamos que queremos insertar 1000 elementos. El proceso sería el siguiente:
Eventos
33 Introducción
Hasta ahora hemos aprendido a acceder a los elementos del DOM y a modificarlos mediante JavaScript. Sin embargo, una página web todavía no resulta interactiva, ya que lo habitual es reaccionar a las acciones realizadas por el usuario:
- Pulsar un botón.
- Escribir en un cuadro de texto.
- Seleccionar una opción.
- Mover el ratón.
- Pulsar una tecla.
- Arrastrar un elemento.
Estas acciones reciben el nombre de eventos. Gracias a ellos, JavaScript puede ejecutar código exactamente en el momento en que ocurre una determinada acción. Los eventos pueden ser provocados por:
- El usuario.
- El navegador.
- El propio documento HTML.
Entre todos los eventos que existen (de elemento, del document, ..), podemos destacar los siguientes por ser de los más utilizados:
| Evento | Acción |
|---|---|
| click | El usuario pulsa un elemento. |
| dblclick | Doble clic. |
| input | Cambia el contenido de un campo de texto. |
| change | Finaliza un cambio en un formulario. |
| submit | Se envía un formulario. |
| keydown | Se pulsa una tecla. |
| keyup | Se libera una tecla. |
| mouseenter | El ratón entra en un elemento. |
| mouseleave | El ratón sale del elemento. |
| load | La página termina de cargarse. |
33.1 Programación dirigida por eventos
Las aplicaciones web modernas funcionan mediante un modelo denominado programación dirigida por eventos. En lugar de ejecutar instrucciones una detrás de otra hasta finalizar el programa, JavaScript permanece esperando a que ocurra algún evento.
Cuando el evento se produce, ejecuta el código asociado y después vuelve a esperar el siguiente evento. Este comportamiento permite que una página permanezca activa e interactiva mientras el usuario la utiliza.
Realmente no es JavaScript quien detecta directamente las acciones del usuario, sino que es el navegador quien observa continuamente lo que ocurre. Cuando detecta un evento, avisa al motor de JavaScript para que ejecute la función correspondiente.
34 Registrar eventos con addEventListener()
Para que JavaScript responda a un evento es necesario registrarlo. Actualmente la forma recomendada consiste en utilizar el método addEventListener() (disponible desde 2015). Este método permite asociar una función a cualquier evento, por lo tanto recibe al menos dos parámetros principales.
- El nombre del evento.
- La función que se ejecutará cuando ocurra.
HTML
<button id="boton">
Pulsar
</button>
<ul id="lista"></ul>
JavaScript
const boton = document.querySelector("#boton");
boton.addEventListener(
"click",
function () {
console.log("Botón pulsado.");
const e = document.createElement("li");
e.textContent = "elemento";
const l = document.getElementById("lista");
l.append(e);
}
);
Con este ejemplo hemos unido crear un evento con la creación de elementos visto anteriormente. Cuando se pulsa el botón se crea un elemento que se añade a la lista, aparte de lanzar un mensaje en consola. En la documentación se pueden ver todas las opciones con las que cuenta addEventListener().
En este caso, hemos creado la función (al estilo clásico) dentro del evento, pero podemos usar una función flecha o llamar a una función ya existente:
Función flecha
boton.addEventListener(
"click",
() => {
console.log("Botón pulsado.");
}
);
Llamar a función existente
boton.addEventListener(
"click",
nuevoElemento
);
Usa el ejemplo anterior y crea 3 botones que lancen eventos click con función completa, flecha y llame a una función existente.
34.1 Un elemento con varios eventos
Un mismo elemento puede responder a distintos eventos.
Elemento con varios eventos
boton.addEventListener(
"mouseenter",
() => { console.log("Ratón encima.");}
);
boton.addEventListener(
"mouseleave",
() => { console.log("Ratón sale del botón.");}
);
34.2 Varias funciones para el mismo evento
Hay situaciones que queremos tener dos o más funciones para el mismo evento, por lo tanto, podemos registrar varias funciones para el mismo evento.
Varias funciones para mismo evento
boton.addEventListener(
"click",
() => console.log("Primera")
);
boton.addEventListener(
"click",
() => console.log("Segunda")
);
Al hacer click sobre el botón se ejecutarán ambas funciones.
34.3 ¿Por qué utilizar addEventListener()?
A la hora de crear eventos con HTML y JavaScript también se puede hacer de las siguientes maneras:
Función inline en HTML
<!-- añadir función en HTML-->
<button onclick="saludar()">
Añadir evento al elemento
// añadir evento al elemento
boton.onclick = function (){
console.log("Hola");
}
boton.onclick = function (){
console.log("Adios");
}
¿Qué pasa al pulsar el botón? Cuál de las tres funciones se ejecuta?
Partiendo del ejercicio anterior:
- Añade al botón1 los eventos mouseenter y mouseleave.
- Añade al botón1 la función inline en HTML onclick.
- Añade dos eventos onclick al elemento del botón1 en JavaScript.
¿Qué se termina ejecutando al hacer click al botón1?
Tal como se ha podido comprobar al hacer el ejercicio, usar addEventListener() añade una serie de ventajas respecto a los métodos “antiguos”:
- Separar HTML y JavaScript facilita el mantenimiento del código.
- Permite registrar varios manejadores para un mismo evento.
- El método inline sólo nos permite añadir una función.
- Nos aseguramos que un evento al elemento no sobreescribe código ya escrito.
- addEventListener() permite añadir más parámetros para controlar la propagación del evento.
Se recomienda utilizar siempre addEventListener().
34.4 Eliminar eventos con removeEventListener()
Hasta ahora hemos aprendido a asociar una función a un evento, pero en algunas situaciones también puede ser necesario eliminar un evento para que deje de ejecutarse. Para ello JavaScript tenemos el método removeEventListener().
Eliminar un evento resulta útil en situaciones como las siguientes:
- Evitar que una acción pueda ejecutarse varias veces.
- Eliminar eventos de elementos que ya no se utilizan.
- Mejorar el rendimiento en aplicaciones complejas.
Sirva el siguiente código como ejemplo, en el que al pulsar el botón Desactivar evento, el botón principal dejará de responder al evento click.
HTML
<button id="boton">
Pulsar
</button>
<button id="quitar">
Quitar evento
</button>
Eliminar evento
const boton = document.querySelector("#boton");
const desactivar = document.querySelector("#quitar");
function saludar() {
console.log("Hola");
}
boton.addEventListener("click", saludar);
desactivar.addEventListener("click", () => {
boton.removeEventListener("click", saludar);
});
Usar removeEventListener() sólo funciona cuando se indica la referencia de una función existente, no valdría con una función inline o función flecha.
Usa removeEventListener() para eliminar un evento creado previamente.
34.5 Opciones de la función
Aparte de los parámetros vistos, el tipo de evento y la función, addEventListener() puede recibir parámetros opcionales diferenciados en:
- options: Un objeto que especifica características sobre el event listener. Entre las opciones disponibles están:
- once: valor booleano que si es true automáticamente se elimina tras ser invocado. Es la alternativa a usar el removeEventListener() visto anteriormente.
- signal: permite asociar una señal.
35 El objeto Event
Cuando ocurre un evento, el navegador genera automáticamente un objeto que contiene toda la información relacionada con él. Este objeto recibe el nombre de Event. JavaScript se lo puede pasar automáticamente como argumento a la función que gestiona el evento. El nombre de la variable que se le pasa a la función suele ser “event""” o “e""” para abreviar.
Mostrar datos del evento
boton.addEventListener(
"click",
(event) => {
console.log(event);
}
);
Mostrar datos del evento
boton.addEventListener(
"click",
(e) => {
console.log(e);
}
);
Cada vez que el usuario haga clic, el navegador mostrará un objeto con abundante información.
35.1 Propiedades de Event
El objeto Event contiene decenas de propiedades y métodos, y en la práctica únicamente se utilizan aquellas necesarias para resolver un problema concreto. Por poner unos ejemplos están los siguientes atributos:
- type: Indica el tipo de evento que ha ocurrido.
- target: Qué elemento ha generado el evento.
- currentTarget: Indica el elemento que tiene registrado el addEventListener(). Aunque puede coincidir con el atributo anterior, no siempre es así, ya que target puede ser un elemento hijo del que tiene registrado el evento.
- clientX y clientY: Las coordenadas del ratón.
- key y code: indica la tecla generada y la tecla física del teclado.
35.2 Interfaces basadas en Event
Aunque todos los eventos generan un objeto Event, existen distintas interfaces especializados. Algunas de ellas:
MouseEvent: tal como dice la documentación, eventos que suceden cuando el usuario interactúa con “un dispositivo de apuntado” (como el ratón).
KeyboardEvent: eventos de interacción con el teclado.
SubmitEvent: este evento actúa cuando la acción submit de un formulario se invoca.
InputEvent: eventos que funcionan con cambios en los contenidos editables.
FocusEvent: eventos que tienen que ver con el “foco”.
TouchEvent: para pantallas táctiles o trackpads.
DragEvent: eventos que representan la interacción drag & drop.
GamepadEvent: es el interfaz con la API de mandos de juegos.
36 Ejemplos de eventos
A continuación vamos a ver distintos ejemplos de eventos usados de manera habitual.
36.1 Eventos de ratón
Todos los ejemplos vistos hasta ahora han tenido en cuenta el evento click que pertenece al ratón y lanzan un MouseEvent. A continuación un nuevo ejemplo junto con el uso de propiedades de Event:
HTML
<button>Rojo</button>
<button>Verde</button>
<button>Azul</button>
Añadir evento al elemento
const botones = document.querySelectorAll("button");
for (const boton of botones) {
boton.addEventListener("click", (e) => {
console.log(e.target.textContent);
console.log(`X,Y: ${e.clientX},${e.clientY}`);
});
}
- Crea tres botones con eventos para obtener información del objeto Event.
- Crea un elemento div con un evento. Que este div contenga a su vez otros dos. Comprueba la diferencia entre target y currentTarget
36.2 Eventos de teclado
Cuando el usuario pulsa una tecla se genera un objeto KeyboardEvent. Podemos generar un evento en el document que funcione como keylogger:
Evento de teclado
document.addEventListener("keydown", (e) => {
console.log(`${e.key} y ${e.code}`);
});
Crea una web que genere una alerta al introducir el código Konami.
36.3 Eventos de entrada
Los elementos de entrada (<input>) de los formulario generan eventos específicos al editarlos o también cuando obtienen el foco.
HTML
<form id="form">
<input id="nombre">
</form>
<pre id="output"></pre>
Añadir evento al input
const form = document.querySelector("#form");
const nombre = document.querySelector("#nombre");
form.addEventListener("input", (e) => {
console.log(e.target.value);
});
nombre.addEventListener("focus", (e) => {
console.log(e.target.value);
});
Cada vez que el usuario escriba una letra aparecerá el contenido actual del cuadro de texto, y también cuando el texto reciba el foco.
Crea un formulario con un input, range y textarea, que loguee quién tiene el foco y el valor al realizar un cambio.
36.4 Eventos de formulario/envío
Los formularios también generan un evento cuando intentan enviarse, al realizar el evento submit
Añadir evento al formulario
const formulario = document.querySelector("form");
formulario.addEventListener("submit", (evento) => {
console.log("Formulario enviado");
});
37 Evitar comportamiento predeterminado
Algunos elementos poseen un comportamiento predeterminado: que un enlace abra otra página o enviar al servidor un formulario. En ocasiones interesa evitar ese comportamiento. Para ello tenemos la función preventDefault().
Evitar comportamiento predeterminado
formulario.addEventListener("submit", (evento) => {
evento.preventDefault();
console.log("Envío cancelado.");
});
38 Parar propagación del evento
Los eventos se propagan por el árbol DOM de abajo-arriba. Cuando un contenedor contiene a otro y ambos escuchan eventos, al lanzar un evento desde el elemento más bajo, el contenedor que contiene ese elemento también recibirá el evento. Para parar esa propagación está la función stopPropagation()
Parar propagación
formulario.addEventListener("submit", (evento) => {
evento.stopPropagation();
});
- Crea un div que contenga otro. Ambos pueden recibir el evento click.
- Con un checkbox controla si quieres parar la propagación del evento.
- Haz click en ambas situaciones: ¿qué ocurre?
Formularios y validación
39 Introducción
Hasta este momento hemos aprendido a crear páginas web dinámicas mediante JavaScript, manipular el DOM y responder a las acciones del usuario mediante eventos. Sin embargo, la mayoría de las aplicaciones web necesitan que el usuario pueda introducir información.
Por ejemplo:
- Iniciar sesión.
- Registrarse.
- Buscar productos.
- Enviar mensajes.
- Realizar pedidos.
- Configurar preferencias.
Toda esta información se recoge mediante formularios, que son un conjunto de controles que permite al usuario introducir información en una página web.
Los datos introducidos pueden utilizarse para distintas finalidades:
- Enviarse a un servidor.
- Procesarse mediante JavaScript.
- Guardarse en el navegador.
- Utilizarse para filtrar o buscar información.
Los formularios constituyen uno de los elementos más utilizados en el desarrollo de aplicaciones web.
40 El elemento <form>
Todos los controles de un formulario suelen agruparse dentro de un elemento <form>, que actúa como un contenedor para todos los controles relacionados.
Formulario básico
<form>
<label for="nombre">Nombre</label>
<input type="text" id="nombre" name="nombre">
<button type="submit">Login</button>
</form>
Tradicionalmente, cuando el usuario pulsa un botón de envío, el navegador recopila todos los datos del formulario y los envía al servidor. Hoy en día JavaScript suele intervenir para comprobar que los datos son correctos.
Es importante siempre realizar la validación también en el backend.
40.1 Atributos principales del formulario
El elemento <form> dispone de numerosos atributos. Los más importantes son los siguientes.
| Atributo | Descripción |
|---|---|
| action | Dirección del servidor a la que se enviarán los datos. |
| method | Método HTTP utilizado (GET para obtener información o POST) para enviar datos. |
| autocomplete | Activa (=on) o desactiva (=off) el autocompletado del navegador. |
| novalidate | Desactiva la validación automática de HTML5. |
| name | Nombre del formulario. Hoy en día se usa más id |
Formulario con atributos
<form action="/login" method="post" autcomplete="on" name="registro">
<label for="nombre">Nombre</label>
<input type="text" id="nombre" name="nombre">
<button type="submit">Login</button>
</form>
Modifica los atributos del formulario y comprueba qué sucede.
40.2 Controles dentro del formulario
Un formulario puede contener numerosos tipos de controles <input>, y cada uno de ellos está diseñado para introducir un tipo concreto de información. Es importante elegir los adecuados ya que van a facilitar la usabilidad de la aplicación y también facilita la validación de los datos. Pueden ser:
- Cuadros de texto.
- Contraseñas.
- Casillas de verificación.
- Botones de opción.
- Listas desplegables.
- Áreas de texto.
- Botones.
40.2.1 Atributo name en los controles
Todos los controles deben contar con un atributo name que será el identificador que se enviará al servidor al enviar el formulario.
Formulario con atributos
<form action="/login" method="post" autcomplete="on" name="registro">
<label for="nombre">Nombre</label>
<input type="text" id="nombre" name="name">
<button type="submit">Login</button>
</form>
En el ejemplo anterior vemos cómo el control input de tipo text cuenta con otros dos atributos, que se les ha dado nombres distintos para diferenciarlos:
- id: es el identificador único HTML que ya hemos visto hasta ahora.
- name: es el nombre del campo que se enviará al servidor al pulsar el botón.
Modifica los atributos id y name e inspecciona los cambios.
40.2.2 Campos de texto
Los controles de texto permiten introducir cadenas de caracteres. Tiene distintos atributos pero el más importante es type:
- type: Indica el tipo de campo de texto que es. Puede facilitar la introducción del texto dependiendo del tipo que sea.
- text: Campo de texto normal.
- password: Para contraseñas, el navegador oculta los caracteres introducidos, pero no está cifrada.
- email: HTML5 comprobará automáticamente que el formato sea estilo email.
- url: Permite introducir direcciones web.
- tel: No valida automaticamente el formato, pero facilita la introducción del dato en dispositivos móviles.
- search: Algunos navegadores incorporan botones para borrar el contenido.
- minlength: Longitud mínima del string.
- maxlength: Longitud máxima que debe tener. Si se llega al límite, no se puede escribir más.
- pattern: Permite escribir una expresión regular que el texto introducido debe coincidir.
- placeholder: Texto que aparece por defecto, pero en el momento en el que el usuario escribe desaparece.
- readonly: Booleano que si está presente no se puede editar el campo.
- size: Tamaño del campo de texto.
Campos de texto
<label for="nombre">Nombre</label>
<input type="text" name="nombre">
<label for="password">Password</label>
<input type="password" name="password" min=6 max=12>
<label for="email">email</label>
<input type="email" name="email" placeholder="hola@example.com">
40.2.3 Campos numéricos
Los campos de tipo numéricos son similares al de texto, pero sólo para números y que suelen tener botones para aumentar o disminuir. Se debe poner como type="number" y también tiene atributos propios:
- max: Número máximo aceptado.
- min: Número mínimo aceptado.
- step: Cantidad para aumentar o disminuir con los botones.
Campos numéricos
<label for="number">number</label>
<input type="number" name="number" min=5>
<label for="number2">number2</label>
<input type="number" name="number2" min=6 max=50 step=2>
40.2.4 Campos de hora/fecha
Existen unos campos específicos que se usan para indicar horas y/o fechas.
- type: Indica el tipo de campo.
- date: Campo para fecha.
- datetime-local: Para fecha y hora local.
- month: Para seleccionar un mes de un año concreto.
- time: Para seleccionar unicamente la hora.
- week: Para seleccionar unicamente la semana.
- max: Dependerá del tipo, pero la hora/fecha máxima aceptada. El formato dependerá del tipo.
- min: Dependerá del tipo, pero la hora/fecha mínima aceptada. El formato dependerá del tipo.
Campos de hora/fecha
<label for="number">date</label>
<input type="date" name="date">
<label for="datetime-local">datetime-local</label>
<input type="datetime-local" name="datdatetime-locale">
<label for="month">month</label>
<input type="month" name="month">
<label for="time">time</label>
<input type="time" name="time">
<label for="week">week</label>
<input type="week" name="week">
40.2.5 Campos de rango
Campos de tipo deslizador para elegir entre un rango de datos. Es de tipo type="range" y también acepta los atributos max, min y step
Campos de rango
<label for="range">range</label>
<input type="range" name="range">
<label for="range2">range2</label>
<input type="range" name="range2" min=0 max=30 step=2>
40.2.6 Campos de casillas y opciones
Permiten seleccionar varias opciones simultáneamente o una única opción entre varias:
- type: Indica el tipo de campo.
- checked: Booleano que indica que la opción está seleccionada.
Las casillas que comparten name pertenecen al mismo grupo.
Campos de casillas y opciones
<fieldset>
<legend>Choose your interests:</legend>
<div>
<input type="checkbox" id="coding" name="interest" value="coding" checked />
<label for="coding">Coding</label>
</div>
<div>
<input type="checkbox" id="music" name="interest" value="music" />
<label for="music">Music</label>
</div>
</fieldset>
<fieldset>
<legend>Please select your preferred contact method:</legend>
<div>
<input type="radio" id="contactChoice1" name="contact" value="email" />
<label for="contactChoice1">Email</label>
<input type="radio" id="contactChoice2" name="contact" value="phone" />
<label for="contactChoice2">Phone</label>
<input type="radio" id="contactChoice3" name="contact" value="mail" />
<label for="contactChoice3">Mail</label>
</div>
</fieldset>
40.2.7 Listas desplegables
Es habitual que en un formulario haya listas desplegables con varias opciones:
Listas desplegables
<label for="hr-select">Your favorite food</label> <br />
<select name="foods" id="hr-select">
<option value="">Choose a food</option>
<hr />
<optgroup label="Fruit">
<option value="apple">Apples</option>
<option value="banana">Bananas</option>
<option value="cherry">Cherries</option>
<option value="damson">Damsons</option>
</optgroup>
<hr />
<optgroup label="Fish">
<option value="cod">Cod</option>
<option value="haddock">Haddock</option>
<option value="salmon">Salmon</option>
<option value="turbot">Turbot</option>
</optgroup>
</select>
40.2.8 Otros controles
Los formularios cuentan con otros controles, algunos de ellos menos habituales, pero que también son útiles para otros tipos de aplicaciones.
- <textarea>: Campo para introducir texto multi-línea.
- rows: Para modificar el número de líneas.
- cols: Para modificar el número de columnas.
- <input type="color">: Selector para elegir un color.
- value: Se puede indicar el color por defecto.
- <input type="file">: Selector para elegir un fichero de nuestro equipo.
- accept: Permite indicar el tipo de fichero que el selector mostrará, excluyendo el resto.
- <input type="hidden">: Permite añadir un campo oculto al formulario para enviar información necesaria que el usuario no tiene por qué rellenar.
Otros campos
<textarea name="textarea" rows="5" cols="30">
Write something here…
</textarea>
<div>
<input type="color" id="foreground" name="foreground" value="#e66465" />
<label for="foreground">Foreground color</label>
</div>
<div>
<label for="avatar">Choose a profile picture:</label>
<input type="file" id="avatar" name="avatar" accept="image/png, image/jpeg" />
</div>
<div>
<input type="hidden" id="postId" name="postId" value="34657" />
</div>
40.2.9 Botones
Los formularios cuentan normalmente con al menos un botón de envío, aunque también se puede añadir uno para resetear los campos.
Botoiak
<!-- método antiguo -->
<input type="submit" value="Enviar">
<input type="reset" value="Reset">
<!-- mejor así -->
<button>Aceptar</button>
<button type="reset">Reset</button>
40.3 Controles obligatorios con required
Es habitual que dentro de un formulario haya campos que sean obligatorios (nombre, contraseña, email,…), y por tanto el usuario deba introducir texto.
Para facilitar esa labor HTML cuenta con el atributo required que recordará al usuario que tiene que rellenar el campo. Esa validación la realiza el navegador.
Control obligatorio
<label for="nombre">Nombre</label>
<input id="nombre" type="text" required>
40.4 Etiquetas y accesibilidad
Un formulario correctamente programado no siempre resulta fácil de utilizar. La accesibilidad y la usabilidad buscan que cualquier usuario pueda completar un formulario de forma cómoda, independientemente del dispositivo utilizado o de sus capacidades. Diseñar formularios accesibles beneficia a todos los usuarios y constituye una buena práctica en el desarrollo web.
Todo control debería disponer de una etiqueta descriptiva, tal como se ha ido poniendo en los ejemplos previos. Esta etiqueta, <label>, tiene la siguiente forma:
Etiqueta para los campos
<label for="nombre">Nombre</label>
<input id="nombre" type="text">
La relación entre el atributo for de la etiqueta e id del control permite que el usuario pueda pulsar sobre la etiqueta para activar el control. Esto es especialmente útil en los campos checkbox y radio.
Para mejorar la usabilidad hay que usar for de la etiqueta e id del control en los checkbox y radio.
40.4.1 Atributo placeholder
Ya hemos visto previamente cómo el atributo placeholder es útil en campos de texto, que permite mostrar un texto de ejemplo. Este atributo no debe sustituir a la etiqueta <label>.
40.4.2 Agrupar controles
Cuando varios controles pertenecen al mismo grupo resulta recomendable utilizar la etiqueta <fieldset> tal como se ha visto previamente. Normalmente utilizada para los campos de selección, pero puede usarse para cualquier tipo de campos.
41 Uso de JavaScript en formularios
Ya se ha visto cómo crear formularios mediante HTML, pero para construir aplicaciones dinámicas es necesario acceder a los controles desde JavaScript para leer su contenido, modificarlo o responder a las acciones del usuario.
En este apartado aprenderemos las operaciones más habituales que se realizan sobre un formulario.
41.1 Obtener controles del formulario
Como cualquier otro elemento del DOM, los controles pueden obtenerse mediante los métodos de selección habituales. Teniendo en cuenta todo lo visto anteriormente, en los siguientes ejemplos se va a seleccionar parte de los controles y cómo obtener el valor de los mismos.
Obtener controles
// obtener un control concreto
const password = document.querySelector("#password");
console.log(password.value);
// un checkbox o radio
const coding = document.querySelector("#coding");
console.log(coding.checked);
// una lista seleccionable
const lista = document.querySelector("#hr-select");
// valor seleccionado
console.log(lista.value);
// índice del seleccionado
let indice = lista.selectedIndex
console.log(indice);
// texto a través del índice
console.log(lista[indice].text)
// obtener todos los inputs en un array
const controles = document.querySelectorAll("input");
Es importante entender que todo lo que devuelve el atributo value es un texto, por lo que si esperamos un número, tendremos que convertirlo.
Por el contrario, para obtener los checkbox y radio tenemos que usar el atributo checked, que nos devolverá un tipo booleano.
En las listas desplegables se puede obtener el item seleccionado y su posición en la lista. La lista empieza desde cero, pero normalmente esa posición se suele usar para un texto estándar de selección. También hay que tener en cuenta que una cosa es el texto del option que se le muestra al usuario y el valor real del value.
Crea un formulario con los controles vistos previamente y que al pulsar el botón del formulario obtener los valores. Usa preventDefault() para ver los valores.
41.2 Modificar valores
Si queremos cambiar el contenido de un control para asignarle un nuevo valor es similar al apartado anterior, sólo que en lugar de obtener el valor, lo que tenemos que hacer es asignarlo.
41.3 Obtener formulario completo
Podemos obtener todo el formulario.
Obtener todo el formulario
const formulario = document.querySelector("#registro");
41.4 Uso del foco en formularios
El foco, focus, indica qué control está preparado para recibir información del usuario o está siendo utilizado en ese instante. También podemos saber cuándo un elemento ha perdido el foco con blur.
Desde JavaScript podemos situar el foco en un element, normalmente es muy útil cuando detectamos un error de validación. Aunque más adelante se verá cómo realizar la validación, ésta se puede hacer cada vez que el usuario escribe/usa un control, o se puede crear un evento con blur para que lo valide cuando pierda el foco.
Evento para validar campo
const password = document.querySelector("#password");
password.addEventListener("blur", (e)=>{
if (password.value.length < 7){
alert("password corta");
}
});
41.5 Habilitar y deshabilitar controles
Los controles pueden activarse o desactivarse mediante la propiedad disabled que es de tipo booleano. En el HTML con poner el atributo es suficiente para indicar que está deshabilitado.
HTML
<input type="text" disabled>
Deshabilitar control
boton.disabled = true;
En el momento en el que se deshabilita el campo no podrá recibir el foco y tampoco se enviará al formulario.
41.6 Atributo readonly
Existe la propiedad readonly para hacer que un control no pueda ser modificado. El usuario puede leer el contenido, el control puede recibir el foco y se envía en el formulario. Se suele utilizar, por ejemplo, cuando al usuario se le asigna un ID único o nombre de usuario que no podrá modificar.
HTML
<input type="text" readonly>
Deshabilitar control
boton.readonly = true;
42 Validación HTML5
HTML5 incorpora un sistema de validación integrado que permite comprobar muchos errores sin escribir código JavaScript. Gracias a esta funcionalidad es posible detectar campos obligatorios, números fuera de rango, correos electrónicos incorrectos y muchas otras situaciones.
Ya se han ido viendo a lo largo de este capítulo varias de los atributos, pero se van a volver a poner aquí:
- required: campo obligatorio.
- minlength y maxlength: para el tamaño del texto.
- min y max: para campos numéricos.
- step: comprobará el campo numérico.
- pattern: comprobará con la expresión regular indicada.
Todas estas validaciones se realizan automáticamente al pulsar el botón de envío del formulario. Esta validación la realiza el navegador, y si es incorrecto suele indicarlo con un mensaje.
42.1 Comprobar desde JavaScript
JavaScript también puede consultar el estado de esta validación con la función checkValidity() para el campo, devolviendo un boobleano.
HTML
<input type="text" minlength="4">
Comprobar validación
nombre.checkValidity();
Además de comprobar el estado, podemos forzar que vuelvan a salir los mensajes de validación del navegador.
Mostrar validación
formulario.reportValidity();
Y podemos modificar el texto que aparece y personalizarlo.
Modificar texto validación
nombre.setCustomValidity(
"Debe introducir su nombre."
);
42.2 Estados CSS
HTML5 incorpora pseudo-clases que indican el estado de un control, por lo que de esta manera podemos personalizarlo a nuestro gusto y adaptarlo a los colores de nuestra aplicación
CSS de validación
input:valid {
border: 2px solid green;
}
input:invalid {
border: 2px solid red;
}
43 Validación mediante JavaScript
Ya se ha visto que HTML5 incorpora numerosas comprobaciones automáticas, pero existen situaciones que requieren reglas más complejas para las que será necesario usar JavaScript. Por ejemplo:
- Comparar dos contraseñas.
- Validar un DNI.
- Verificar que una fecha sea posterior a otra.
43.1 Validación en tiempo real
Podemos validar mientras el usuario escribe dentro de un campo de texto:
Validación tiempo real
nombre.addEventListener("input", () => {
if (nombre.value.length < 3) {
console.log("Nombre demasiado corto.");
}
});
Cuando validamos en tiempo real se detectan automáticamente los errores y podemos avisar al usuario antes de que siga rellenando el resto del formulario. De esta manera no recibirá todos los posibles errores a la vez.
43.2 Validación al enviar formulario
También es posible realizar todas las comprobaciones cuando el usuario pulsa el botón de envío, una vez rellenado todo el formulario:
Validación tiempo real
formulario.addEventListener("submit", (e) => {
if (nombre.value === "") {
e.preventDefault();
alert("Debe introducir un nombre.");
nombre.focus();
}
});
Una ventaja para usar este método es que toda la validación podría estar dentro de una misma función. Tal como se puede ver en el ejemplo, se ha realizado la validación, y en caso de no ser válida, se realiza un aviso al usuario y se pone el foco en el campo correspondiente.
Cuando se realiza la validación con preventDefault() se evita enviar el formulario.
Crea un formulario que valide algún campo en tiempo real y todos al enviar el formulario.
43.3 Combinar HTML5 y JavaScript
En la mayoría de aplicaciones se utilizan ambas técnicas: HTML5 valida automáticamente los errores más sencillos mientras que con JavaScript se realizan las comprobaciones específicas de la aplicación. Esta combinación reduce considerablemente la cantidad de código necesario.
44 Mensajes de error y experiencia de usuario
Validar correctamente un formulario no consiste únicamente en impedir que el usuario introduzca datos incorrectos, también es importante informarle de forma clara sobre qué error ha cometido y cómo puede corregirlo.
Un formulario que muestra mensajes adecuados resulta mucho más fácil de utilizar que otro que simplemente impide el envío de los datos. Un buen mensaje debería responder a tres preguntas:
- ¿Qué ha ocurrido?
- ¿Por qué se ha producido el error?
- ¿Cómo puede solucionarse?
Los mensajes de error deben ser claros, breves, específicos, educados y fáciles de comprender para un usuario no-técnico.
El error “pattern mismatch” puede ser útil para debuggear pero no para el usuario.
Aunque ya se ha ido indicando en apartados anteriores cómo realizar una buena experiencia de usuario, a continuación se vuelve a recopilar de cara a tener buenas prácticas en el desarrollo.
44.1 Mostrar mensajes junto al campo
Siempre que sea posible, el mensaje debe aparecer cerca del control que contiene el error, de esta manera el usuario identifica inmediatamente qué dato debe corregir. Para ello se puede utilizar elementos HTML que en principio estén ocultos y que en caso de error aparezcan:
HTML
<label for="nombre">
Nombre
</label>
<input
id="nombre"
type="text">
<span id="errorNombre"
class="hidden error">
</span>
Comprobar validación
const nombre = document.querySelector("#nombre");
if (nombre.value === ""){
const er = document.querySelector("#errorNombre");
er.textContent = "Debe introducir un nombre.";
er.mensaje.classList.replace("hidden", "");
}
Si la validación se está haciendo en tiempo real, una vez el usuario corrige el error, el mensaje de error debe desaparecer.
44.1.1 Resaltar el campo
Aparte del mensaje de error, resulta recomendable indicar visualmente el control con un color diferente, para que resalte cuál es el que contiene el error. Previamente se ha indicado que HTML5 cuenta con unas clases CSS especiales, pero podemos crear unas clases propias.
HTML
.error {
border: 2px solid red;
}
JavaScript
// añadir y quitar clase
nombre.classList.add("error");
nombre.classList.remove("error");
En la siguiente imagen se puede apreciar el formulario de registro de usuario en el que se ha optado por añadir todos los mensajes de error al inicio del formulario y resaltar cada campo que contiene un error.
44.2 No borrar la información introducida
Cuando el formulario contiene errores, el usuario espera poder corregir únicamente los campos incorrectos. No debería ser necesario volver a escribir todos los datos. Por este motivo, normalmente no debe vaciarse el formulario después de una validación fallida.
Es importante para la usabilidad no vaciar los campos rellenados cuando se realiza la validación.
44.3 Poner foco en el campo
Ya se ha visto previamente que es recomendable situar el cursor sobre el campo que se ha validado y ha dado error. De esta manera, en caso de ser un formulario grande, el usuario podrá comenzar inmediatamente con la corrección.
45 Ejercicios con formularios
A lo largo de esta unidad hemos estudiado cómo crear formularios HTML, acceder a sus controles desde JavaScript y validar la información introducida por el usuario. En este apartado reuniremos todos estos conocimientos desarrollando unos ejercicios.
El objetivo no es crear una aplicación real, sino integrar en un único ejemplo las técnicas más utilizadas en el desarrollo web.
Crea un formulario que tenga los siguientes campos:
- Nombre
- Correo electrónico
- Edad
- Contraseña
- Confirmación de contraseña
- Aceptación de las condiciones de uso
Antes de enviarlo se comprobará que:
- Todos los campos obligatorios estén rellenados.
- El usuario sea mayor de edad.
- Las dos contraseñas coincidan.
- Se hayan aceptado las condiciones de uso.
Usa validación propia y de HTML, y si hay error que aparezcan errores y cambie el CSS.
Crea un formulario para la web del centro para registrar nuevos alumnos. Debe contener:
- Nombre
- Apellidos
- Correo electrónico
- Edad
- Curso
- Turno (mañana o tarde)
- Comentarios
- Aceptación de la política de privacidad
El formulario deberá cumplir los siguientes requisitos:
- Todos los campos serán obligatorios excepto “Comentarios”.
- La edad deberá estar comprendida entre 16 y 99 años.
- El correo electrónico deberá utilizar el tipo de entrada adecuado de HTML5.
- El alumno deberá aceptar la política de privacidad.
- Si existe algún error se mostrará un mensaje indicando el problema y el formulario no se enviará.
- Si todos los datos son correctos aparecerá un mensaje indicando que el registro se ha realizado correctamente.
- El formulario deberá disponer de un diseño sencillo mediante CSS.
Programación asíncrona
46 Programación asíncrona
Hasta este momento todos los programas que hemos desarrollado han tenido una característica común: las instrucciones se ejecutan una detrás de otra. Sin embargo, muchas aplicaciones necesitan realizar tareas que pueden tardar un tiempo en completarse:
- Descargar información de un servidor.
- Esperar a que el usuario pulse un botón.
- Mostrar una animación.
- Ejecutar una acción varios segundos después.
- Leer un archivo.
- Obtener la posición GPS del dispositivo.
Si JavaScript esperase a que cada una de estas operaciones finalizara antes de continuar ejecutando el programa, las páginas web quedarían bloqueadas y dejarían de responder al usuario.
Para evitar este problema, JavaScript incorpora un modelo de programación asíncrona, que permite iniciar determinadas tareas y continuar ejecutando el resto del programa mientras dichas tareas finalizan.
46.1 Operaciones síncronas vs asíncronas
Una operación síncrona es aquella que debe finalizar antes de que el programa continúe ejecutando la siguiente instrucción. Es decir, las instrucciones se ejecutan estrictamente en orden y hasta que no termina una instrucción no comienza la siguiente.
Una operación asíncrona permite que el programa continúe ejecutándose mientras dicha operación finaliza. Cuando la operación termina, JavaScript ejecutará el código correspondiente.
| Programación Síncrona | Programación Asíncrona | |
|---|---|---|
| Resumen | Las instrucciones esperan unas a otras. | Algunas operaciones se ejecutan en segundo plano. |
| Ventajas | ● Es sencilla de comprender. ● Las instrucciones siguen un orden claro. ● Resulta fácil localizar errores. |
● La interfaz continúa respondiendo. ● Adecuada para operaciones lentas. ● Adecuada para peticiones online. |
| Inconvenientes | El programa puede quedar bloqueado. | Flujo más complejo. |
Imaginemos el siguiente código JavaScript:
Código JavaScript
function tarea(message) {
// emula tarea que consume tiempo
let n = 10000000000;
while (n > 0){
n--;
}
console.log(message);
}
console.log('Empieza');
task('Llamamos API');
console.log('Terminado');
Mientras se ejecuta el paso 2, la función tarda tiempo y por tanto todo el resto de procesos quedan bloquedos y a la espera. Esto es un proceso a evitar. Cuando utilizamos una aplicación web se realizan continuamente operaciones asíncronas:
- Consultar datos de una web externa
- Cargar imágenes.
- Reproducir un vídeo.
- Esperar un clic del usuario.
- Esperar una pulsación del teclado.
47 JavaScript y la programación asíncrona
JavaScript ejecuta el código de forma secuencial, pero incorpora mecanismos para gestionar operaciones asíncronas. Todos estos mecanismos se apoyan en un componente fundamental del lenguaje: el Event Loop. Entre estas operaciones podemos destacar:
- Temporizadores
- Eventos
- Promesas
- async / await
- fetch()
47.1 El Event Loop
El Event Loop (bucle de eventos) es el mecanismo encargado de coordinar la ejecución del código JavaScript con las operaciones asíncronas. Aunque internamente es un proceso complejo, podemos entenderlo de forma simplificada. Su trabajo consiste en responder continuamente a la siguiente pregunta:
- ¿Hay alguna tarea pendiente que ya haya terminado?
- Si la respuesta es afirmativa, ejecuta el código asociado a esa tarea.
JavaScript es single-threaded, sólo puede ejecutar una instrucción de código a la vez, por lo que hasta que no aparecen las tareas asíncronas no entra en juego el Event Loop. Puede parecer que se hacen más tareas, pero algunas de ellas son hechas por el navegador. Para entender cómo se ejecuta el código debemos conocer a los “jugadores”:
- Call Stack: Donde las funciones son ejecutadas.
- Web APIs: Provistas por el navegador para tareas asíncronas como setTimeout, peticiones HTTP, o eventos DOM.
- Task Queue (Macro-task queue): Mantiene las callbacks de tareas largas como las de setTimeout o setInterval.
- Micro-task Queue: Mantiene las callbacks de Promise.then, las tareas de MutationObserver y los trabajos de queueMicrotask.
- Event Loop: El coordinador que comprueba: “¿Está vacía la pila de llamadas? Si es así, pasa a la siguiente tarea de la cola.”
JavaScript da más prioridad a las micro-task que a las macro-task y se puede comprobar con el siguiente ejemplo:
Ejemplo de medium.com
console.log("1");
setTimeout(() => {
console.log("2 - macro-task");
}, 0);
Promise.resolve().then(() => {
console.log("3 - micro-task");
});
console.log("4");
Aunque todavía no entendamos qué realiza cada parte, la salida va a ser la siguiente:
- 1
- 4
- 3 - micro-task
- 2 - macro-task
JavaScript da más prioridad a las micro-task que a las macro-task.
Un ejemplo más sencillo en el que se crea un temporizador de dos segundos
Ejemplo sencillo asíncrono
console.log("Inicio");
setTimeout(() => {
console.log("Han pasado dos segundos.");
}, 2000);
console.log("Fin");
Con la programación asíncrona es importante entender que el orden del código no siempre coincide con el orden de ejecución
Comprueba el funcionamiento de los ejemplos previos.
48 Temporizadores
Los temporizadores permiten ejecutar código después de un determinado tiempo o de forma repetitiva. Son una de las primeras herramientas de programación asíncrona disponibles en JavaScript.
48.1 setTimeout()
La función setTimeout() ejecuta una función una única vez cuando ha transcurrido el tiempo indicado, en milisegundos (1000ms == 1s).
Ejemplo con setTimeout()
console.log("Inicio");
const id = setTimeout(() => {
console.log("Han pasado tres segundos.");
}, 3000);
console.log("Fin");
La función setTimeout() devuelve un identificador que podremos utilizar para cancelarlo.
48.2 clearTimeout()
Permite cancelar un temporizador antes de que se ejecute.
Ejemplo con setTimeout()
const id = setTimeout(() => {
console.log("Han pasado cinco segundos.");
}, 5000);
clearTimeout(id);
Esta función es útil para:
- Cancelar una cuenta atrás.
- Detener una notificación.
- Evitar ejecutar una acción si el usuario cambia de opinión.
48.3 setInterval()
La función setInterval() ejecuta una función repetidamente, cuando pasen los milisegundos indicados.
Llama a una función
setInterval(hola(), 2000);
Usa función flecha
setInterval(() => {
console.log(new Date());
}, 1000);
El mensaje seguirá apareciendo cada segundo hasta detener el temporizador.
48.4 clearInterval()
Permite detener un temporizador creado mediante setInterval().
Parar temporizador
let contador = 0;
const reloj = setInterval(() => {
contador++;
console.log(contador);
if (contador === 10) {
clearInterval(reloj);
}
}, 1000);
Comprueba el funcionamiento de los ejemplos con temporizadores.
48.5 Buenas prácticas con temporizadores
Es importante hacer uso de buenas prácticas cuando hacemos uso de temporizadores:
- Guardar el identificador devuelto por los temporizadores.
- Cancelar los temporizadores que ya no sean necesarios.
- Evitar intervalos demasiado cortos, ya que pueden afectar al rendimiento de la aplicación.
49 Callbacks
Un callback es una función que se pasa como argumento a otra función para que sea ejecutada más tarde. Es decir, en lugar de ejecutar una función inmediatamente, se entrega a otra función para que la invoque cuando sea necesario.
En el apartado anterior hemos aprendido a utilizar temporizadores, que a la hora de setearlos reciben como primer parámetro una función. Esa función es ejecutada cuando ocurra un determinado evento (en este caso, cuando transcurra un tiempo).
Los callbacks fueron durante muchos años el principal mecanismo para realizar programación asíncrona en JavaScript. Actualmente siguen utilizándose, aunque en muchas situaciones han sido sustituidos por las promesas y por async / await, que estudiaremos más adelante.
Ejemplo de callback en temporizador
function saludar() {
console.log("Hola");
}
setTimeout(saludar, 3000);
En este caso:
- saludar es el callback.
- setTimeout() decidirá cuándo ejecutarlo.
En el ejemplo anterior se ha pasado una función ya existente, pero se puede pasar funciones anónimas y flecha
Función anónima
setTimeout(function () {
console.log("Hola");
}, 3000);
Función flecha
setTimeout(() => {
console.log("Hola");
}, 3000);
49.1 Cuándo usar callbacks
La palabra inglesa callback significa literalmente “llamar de vuelta”, por lo que la idea es sencilla, proporcionar a una función otra que será llamada cuando sea necesario.
Sirva como ejemplo la siguiente función que realiza una operación, y tiene como tercer parámetro una función callback para ver qué hacer con el resultado. La función no sabe qué va a pasar con el resultado, pero usará el callback para hacerlo.
Ejemplo de callback
function sumar(a, b, callback) {
const resultado = a + b;
callback(resultado);
}
A la hora de llamar a la función podemos decidir qué hacer en el callback que le pasamos como parámetro.
Loguea el resultado
sumar(4, 6, (resultado) => {
console.log(resultado);
});
Lo dibuja en un elemento
sumar(4, 6, (resultado) => {
let r = document.getElementById("r")
r.textContent = resultado
});
En la primera llamada se loguea el resultado, mientras que en la segunda se modifica un elemento HTML.
Crea una función que reciba un callback como parámetro. Llama a la función con un callback diferente.
49.2 Callbacks en los eventos
Ya hemos estado utilizando callbacks sin darnos cuenta.
Callback en evento
boton.addEventListener("click", () => {
console.log("Pulsado");
});
La función flecha anterior es un callback que el navegador ejecutará cuando el usuario pulse el botón.
Todos los eventos que hemos visto hasta ahora hacen uso de callbacks, y pasa lo mismo con los temporizadores.
Todos los eventos y temporizadores tienen callbacks.
49.3 Callback hell
Cuando una aplicación comienza a crecer, los callbacks pueden complicar el código. Cuando varios callbacks se anidan unos dentro de otros aparece un problema conocido como Callback Hell. También se conoce como Pyramid of Doom o Pirámide de la perdición.
El nombre viene del aspecto del código, que recuerda a una pirámide debido al nivel de indentación. Visualmente el código resulta difícil de seguir y además, cualquier modificación puede afectar a varios niveles de la pirámide.
Callback hell
login(usuario, () => {
obtenerPerfil(() => {
obtenerPedidos(() => {
obtenerFactura(() => {
console.log("Proceso terminado");
});
});
});
});
La gestión de errores mediante callbacks tampoco resulta sencilla. Cada operación debería comprobar si ha ocurrido algún problema antes de continuar. Esto incrementa todavía más la complejidad del código.
49.4 Ventajas e inconvenientes
Los callbacks supusieron un gran avance en JavaScript porque permitían realizar tareas asíncronas sin bloquear la ejecución del programa. Sin embargo, también presentan algunos inconvenientes que se pueden ver en la siguiente tabla.
| Ventajas | Inconvenientes |
|---|---|
| ● Permiten ejecutar código cuando ocurre un evento. ● Evitan bloquear la aplicación. ● Son sencillos para operaciones pequeñas. ● Están disponibles en prácticamente todas las APIs del navegador. ● Siguen utilizándose en numerosos métodos de JavaScript. |
● El ódigo puede ser difícil de mantener. ● Muchas funciones anidadas. ● Gestión de errores complicada. ● Baja legibilidad. ● Difícil reutilización del código. |
50 Promesas (Promise)
En el apartado anterior hemos visto que los callbacks permiten ejecutar código cuando finaliza una operación asíncrona. Sin embargo, cuando varias operaciones dependen unas de otras, el código puede llegar a ser difícil de leer y mantener.
Para solucionar este problema, ECMAScript 2015 (ES6) incorporó un nuevo mecanismo denominado Promise (promesa), que permiten representar el resultado de una operación asíncrona y facilitan el encadenamiento de varias operaciones sin producir el conocido Callback Hell. Actualmente son uno de los mecanismos fundamentales de la programación asíncrona en JavaScript.
Cuando creamos una promesa todavía no conocemos el resultado de dicha operación. Sólo sabemos que, en algún momento, ocurrirá una de estas dos situaciones:
- La operación finalizará correctamente.
- La operación producirá un error.
50.1 Estados de un Promise
Toda promesa pasa por distintos estados, que se pueden simplificar en el siguiente diagrama:
- Pending: Es el estado inicial. La operación todavía no ha finalizado.
- Fulfilled: La operación ha terminado correctamente. La promesa devuelve un resultado.
- Rejected: La operación ha producido un error. La promesa devuelve el motivo del fallo.
50.2 Crear una Promise
La forma para crear una Promise es la que aparece en los siguientes ejemplo. La función recibe dos parámetros especiales que son dos funciones (callbacks) capaces de resolver o rechazar la promesa:
- resolve(): Indica que la operación ha finalizado correctamente. La promesa pasa a estado fulfilled.
- reject(): Indica que la operación ha producido un error. Pasa la promesa a estado rejected.
A continuación se crean dos ejemplos:
- Ejemplo sencillo con una condición que resuelve o rechaza la promesa.
- Ejemplo que resolverá o rechazará la promesa al de dos segundos, debido al temporizador, para simular una petición asíncrona (una petición a una web, por ejemplo).
Crear Promise
const p=new Promise((resolve,reject)=>{
// Tareas asíncronas
// cambiar a false para ver error
const success = true;
if (success) {
resolve("éxito");
} else {
reject("error");
}
});
Crear Promise
const p=new Promise((resolve,reject)=>{
// Tareas asíncronas
// cambiar a false para ver error
const success = true;
setTimeout(() => {
if (success) {
resolve("éxito")
} else {
reject("error")
}
}, 2000)
});
50.3 Consumir la Promise
Una vez creada la Promise hay que consumirla para poder obtener el valor de la función realizada de manera asíncrona. Para ello tenemos los siguientes métodos:
- then(): Se ejecuta cuando la Promise se resuelve con éxito.
- catch(): Se ejecuta cuando la Promise se rechaza con error.
- finally(): Se ejecuta siempre, al terminar el proceso de la promesa (después del then() o el catch()). No recibe parámetros y no puede cambiar el valor de la promise. Esta función es útil para:
- Ocultar indicadores de carga.
- Liberar recursos.
- Restaurar la interfaz.
Consumir Promise
promesa
.then((respuesta)=>{
console.log(`Éxito: ${respuesta}`)
})
.catch((error)=> {
console.log(`Error: ${error}`)
})
.finally(()=>{
console.log("Promesa finalizada.")
});
then() y catch son mutuamente excluyentes. Se ejecuta sólo una.
Crea Promises que terminen en distintos estados. Usa finally() como función final.
50.4 Encadenar operaciones
Es posible utilizar varios métodos then() de manera encadenada. Realmente un then() devuelve una nueva Promise, por lo que permite crear una cadena de transformación de resultados, ya que cada uno de ellos puede pasar su resultado al siguiente:
Encadenar operaciones
promesa
.then((respuesta)=>{
console.log(`Éxito 1: ${respuesta}`)
return respuesta;
})
.then((respuesta)=> {
r = respuesta.toUpperCase();
console.log(`Éxito 2: ${r}`);
return r;
})
.then((respuesta)=> {
r = respuesta.toLowerCase();
console.log(`Éxito 3: ${r}`);
});
Tal como se puede ver en el ejemplo anterior se realizan las siguientes operaciones:
- En el primer then() recibimos la respuesta de la promesa, y en este caso lo logueamos y hacemos un return de la respuesta.
- Este texto/objeto se recibe en el siguiente then() como parámetro, que a su vez realiza ciertas operaciones y devuelve otro texto/objeto.
- En el último then() recibimos como parámetro el return del anterior y realizamos una tercera operación.
Todo esto se puede simplificar si hacemos llamadas a funciones ya escritas previamente.
Encadenar operaciones
promesa2
.then(respuesta => igual(respuesta))
.then(respuesta => mayus(respuesta))
.then(respuesta => minus(respuesta));
Y podemos mejorar aún más, ya que podemos pasar directamente las funciones que queremos ejecutar en cada momento, y recibirán el resultado del paso anterior sin necesidad de anidar las funciones. Esto sucede porque el contrato de la función recibe un único parámetro y devuelva una promesa, exactamente como la función que ya estamos creando:
Encadenar operaciones
promesa3
.then(igual)
.then(mayus)
.then(minus);
Crea promesas con operaciones encadenadas y usa las tres formas de escribir las operaciones.
50.5 Ventajas sobre callbacks
A continuación un ejemplo de cómo se podría realizar las mismas operaciones con callbacks y con Promises:
Callback hell de login de usuario 123
login(123, (error,usuario) => {
if (error) {
console.log("Error de login");
} else {
obtenerPerfil(usuario.id,(error,perfil) => {
if (error){
console.log("Error de perfil");
} else {
obtenerPedidos(usuario.id, (error, pedidos) => {
if (error){
console.log("Error en pedidos");
} else {
obtenerFactura(pedidos[0].id,(error,factura) => {
if (error){
console.log("Error en factura")
} else {
// obtener total, productos...
}
});
}
});
}
});
}
});
En este ejemplo de callback hell se ha añadido la gestión de errores y comprobaciones que en el anterior apartado no se habían indicado. Tal como se puede ver, esto incrementa la dificultad a la hora de tener que realizar modificaciones en el futuro.
Si por el contrario usamos Promise quedaría de una de las dos siguientes maneras, dependiendo de si usamos la versión “larga” o abreviada.
Login de usuario
obtenerUsuario(123)
.then((usuario) => {
return obtenerPerfil(usuario.id);
})
.then((usuario) => {
return obtenerPedidos(usuario.id);
})
.then((pedidos) => {
return obtenerFactura(pedidos[0].id);
})
.then((factura) => {
console.log(factura);
})
.catch((error) => {
console.error(error);
});
Login de usuario
obtenerUsuario(123)
.then(obtenerPerfil)
.then(obtenerPedidos)
.then(obtenerFactura)
.then(mostrarFactura)
.catch((error) => {
console.error(error);
});
50.6 Promise.all()
Permite esperar a que finalicen varias promesas para ejecutar después una función, pero sólo se ejecutará cuando todas hayan terminado correctamente:
Esperar promesas
Promise.all([
promesa1,
promesa2,
promesa3
])
.then((resultados) => {
console.log(resultados);
});
Si cualquiera de las promesas produce un error, Promise.all() finaliza inmediatamente con dicho error.
50.7 Promise.race()
Devuelve el resultado de la primera promesa que finalice, da igual cuál sea.
Esperar promesas
Promise.race([
promesa1,
promesa2
])
.then((resultado) => {
console.log(resultado);
});
50.8 Promise.allSettled()
Sirve para esperar el resultado de varias Promise pero sin importar si algunas fallan. Útil para hacer un reporte de estado, para saber qué funcionó y qué no.
Con Promise.allSettled() siempre vamos a obtener un array de objetos de tipo {status, value/reason}.
- status: podrá tener el valor fulfilled para las promesas cumplidas y rejected para las rechazadas
- El segundo campo será:
- value: si la promesa ha tenido éxito, tendremos el resultado.
- reason: si la promesa ha fallado, tendremos el error.
Crea ejemplos con Promise.all(), Promise.race() y Promise.allSettled()
51 async y await
Las promesas solucionaron gran parte de los problemas de los callbacks. Sin embargo, cuando varias promesas se encadenan, el código puede seguir siendo más complejo de lo deseable.
Para simplificar todavía más la programación asíncrona, ECMAScript 2017 incorporó las palabras reservadas async y await, cuyo objetivo consiste en escribir código asíncrono con un aspecto muy parecido al código secuencial.
El objetivo de async y await es escribir código asíncrono con un aspecto similar al código secuencial.
51.1 Declarar función async
Usando la palabra reservada async declaramos que una función sea asíncrona y siempre devuelve una promesa. Podemos crear en funciones estándar o flecha.
Función async
async function saludo() {
return "Hola";
}
Función async flecha
const saludo = async () => {
return "Hola";
};
Aunque aparentemente devuelve una cadena, en realidad devuelve una promesa resuelta con ese valor.
51.2 Esperar función con await
La palabra reservada await permite esperar el resultado de una promesa. Mientras la promesa no finalice, la función asíncrona permanecerá en espera. Hay que recordar que esto no bloqueará todo el programa, si no que JavaScript dejará de lado esta función y seguirá haciendo caso a otra parte del programas.
Función que devuelve promesa
function esperar() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Finalizado");
}, 2000);
});
}
Función asíncrona que espera
async function ejemplo() {
console.log("Inicio");
const resultado = await esperar();
console.log(resultado);
console.log("Fin");
}
ejemplo();
En el ejemplo anterior parece que se está ejecutando síncronamente, pero se ha utilizado las funciones de JavaScript que permite no bloquear el resto de la aplicación.
Crea 3 botones que realicen lo siguiente:
- Loguee “Botón 1 pulsado”
- Llama a la función task del ejercicio 18a. Intenta pulsar el botón 1. ¿Qué pasa?
- Llama a una función async como el ejemplo anterior y espera con await. Intenta pulsar el botón 1. ¿Qué pasa?
A continuación una comparación de cómo sería un ejemplo con promesas y otro con async/await.
Con promesas
esperar()
.then((resultado) => {
console.log(resultado);
});
Ejemplo async/await
const resultado = await esperar();
console.log(resultado);
51.3 Tratamiento de errores
Cuando una promesa produce un error, normalmente utilizaremos try…catch.
Tratamiento de errores
async function ejemplo() {
try {
const datos = await obtenerDatos();
console.log(datos);
}
catch(error){
console.log(error);
}
}
Aunque ya estudiamos try…catch anteriormente, a partir de este momento será especialmente importante, ya que constituye el mecanismo habitual para gestionar errores en funciones asíncronas.
51.4 Varias operaciones
Con las Promise hemos visto cómo concatenar operaciones. Ahora podemos hacer llamadas a varias funciones y esperar l resultado, siendo más legible:
Con promesas
obtenerDatos()
.then((datos) => {
return procesar(datos);
})
.then((resultado) => {
console.log(resultado);
})
.catch((error) => {
console.log(error);
});
Función async/await
async function ejemplo() {
try {
const datos = await obtenerDatos();
const resultado = await procesar(datos);
console.log(resultado);
}
catch(error){
console.log(error);
}
}
El problema de esto es que estamos programando de manera secuencial, por lo que estamos haciendo que el tiempo de ambas funciones se sumen. Para eso es importante recordar que podemos esperar en paralelo con Promise.all.
Esperando en paralelo
async function perfilUsuario(id) {
try {
const [datos, facturas] = await Promise.all([
obtenerDatos(id),
obtenerFacturas(id)
])
console.log(facturas);
}
catch(error){
console.log(error);
}
}
Crea 3 botones que llamen a funciones diferentes que a su vez usen un temporizador de 3, 2 y 1 un segundo. ¿Qué forma tarda menos en ejecutarse?
- Con Promise anidando then
- Con async/await secuencial.
- Con async/await con Promise.all en paralelo.
51.5 ¿Cuándo utilizar async y await?
Siempre que trabajemos con promesas y necesitemos realizar varias operaciones consecutivas. Actualmente constituyen el enfoque recomendado para el desarrollo de aplicaciones JavaScript modernas.
Comunicación con servidores
52 Fetch API
La Fetch API es una interfaz proporcionada por el navegador que permite realizar peticiones HTTP de forma asíncrona. Su función principal es comunicarse con servidores web para de esta manera:
- Descargar información.
- Enviar formularios.
- Consultar una API REST.
- Subir archivos.
- Eliminar información de un servidor.
La función fetch() devuelve una Promise, por lo tanto vamos a poder ver todo lo visto en el capítulo anterior: .then, .catch, async y/o await.
52.1 Evolución de las comunicaciones asíncronas en JavaScript
En los primeros años de JavaScript no existía una forma sencilla de realizar peticiones HTTP desde el navegador sin recargar la página completa. Para ello apareció XMLHttpRequest (XHR), una API introducida por Microsoft en Internet Explorer 5.0 en 1999 (utilizando ActiveX). Después le siguió Mozilla en 2002 y Safari en 2004. En 2006 el W3C presentó el primer borrador para crear un estándar.
A partir de esta tecnología surgió el término AJAX (Asynchronous JavaScript and XML). Es importante destacar que AJAX no es una librería ni una función, sino una técnica de desarrollo que consiste en utilizar JavaScript y XMLHttpRequest para intercambiar información con el servidor sin recargar la página.
En aquella época el uso de XML era el formato estándar de facto para la comunicación “entre máquinas” (por red), de ahí que en su nombre hace referencia a XML. Con el tiempo el formato JSON sustituyó casi por completo a XML debido a su sencillez.
Durante la época de mayor popularidad de AJAX, la compatibilidad entre navegadores era uno de los principales problemas para los desarrolladores. Aunque todos ofrecían la API XMLHttpRequest, existían diferencias en su implementación y comportamiento, especialmente entre Internet Explorer, Firefox, Opera y los primeros navegadores basados en WebKit.
jQuery simplificó enormemente esta tarea al proporcionar una interfaz única para realizar peticiones AJAX mediante funciones como $.ajax(), $.get() o $.post(). De esta forma, el mismo código funcionaba de manera muy similar en los distintos navegadores, evitando que el desarrollador tuviera que programar soluciones específicas para cada uno de ellos.
Con la llegada de ECMAScript moderno apareció la Fetch API, que ofrece una forma mucho más sencilla y legible de realizar peticiones HTTP y es un estándar que todos los navegadores cumplen.
| Tecnología | ¿Qué es? | Situación actual |
|---|---|---|
| XMLHttpRequest (XHR) | API del navegador para realizar peticiones HTTP. | Sigue siendo compatible, pero cada vez se utiliza menos. |
| AJAX | Técnica de desarrollo basada originalmente en XMLHttpRequest. |
El concepto sigue vigente, aunque normalmente se implementa mediante fetch(). |
| Fetch API | API moderna para realizar peticiones HTTP basada en Promesas. | Es la opción recomendada en el desarrollo web actual. |
52.2 Hacer petición simple
La forma más sencilla consiste en indicar únicamente la dirección del recurso/servidor y tal como se ha dicho, como devuelve un Promise, en este caso vamos a usar then para obtener la respuesta.
Hacer petición fetch
fetch("https://jsonplaceholder.typicode.com/users")
.then((respuesta) => {
console.log(respuesta);
});
La variable respuesta contiene la información sobre la respuesta HTTP realizada al servidor, no contiene los datos que necesitamos.
Al hacer fetch primero obtenemos la respuesta HTTP, no contiene los datos que buscamos.
52.2.1 El objeto Response
La respuesta devuelta por fetch() es un objeto de tipo Response. Este objeto contiene información como:
- status: Código de respuesta de la petición HTTP.
- 200: petición correcta.
- 201: recurso creado.
- 301: movido permanentemente.
- 401: no autorizado.
- 404: recurso no encontrado.
- 418: soy una tetera
- 500: error interno del servidor.
- ok: devuelve un booleano para saber si la petición ha sido correcta (entre 200 y 299)
- headers: Cabeceras HTTP.
- Tipo de contenido.
- Cuerpo de la respuesta.
Objeto Response
fetch("https://jsonplaceholder.typicode.com/users")
.then((respuesta) => {
console.log(respuesta.status);
console.log(respuesta.ok);
if (!respuesta.ok) {
throw new Error("Error en la petición.");
}
});
52.2.2 Obtener el contenido
Para obtener los datos que se han solicitado al servidor primero debemos convertir la respuesta a un formato adecuado. Podemos elegir entre los siguientes métodos:
- json(): convierte los datos a formato JSON. Método para comunicaciones con APIs.
- text(): obtiene como texto plano.
- blob(): para archivos binarios (imágenes, vídeos, archivos).
- arrayBuffer(): para binarios crudos, manipulación avanzada de archivos.
- formData(): para formularios.
Elegiremos el formato JSON, ya que de esta manera podremos acceder de manera más sencilla a los datos ya que se convertirán en un objeto fácil de manejar con JavaScript.
La llamada a json() también devuelve una promesa, y por eso necesitamos un segundo then() o un segundo await dependiendo del modo elegido.
La llamada a json() también devuelve una promesa.
Con Promise
fetch(URL)
.then((respuesta) => {
return respuesta.json();
})
.then((datos)=>{
console.log(datos)
})
.catch((error)=> {
console.log(error);
});
Con async / await
async function cargarUsuarios() {
const respuesta = await fetch(URL);
const usuarios = await respuesta.json();
console.log(usuarios);
}
cargarUsuarios();
52.3 Gestión de errores
Para la gestión de errores a la hora de realizar peticiones usaremos try…catch.
Gestionar errores de peticiones
const URL = "https://jsonplaceholder.typicode.com/users";
async function cargarDatos() {
try {
const respuesta = await fetch(URL);
const datos = await respuesta.json();
console.log(datos);
}
catch(error){
console.error(error);
}
}
Crea 2 botones que llamen a sendas API usando promesas y async/await para obtener los datos respectivamente.
53 Comunicación con APIs REST
Una de las aplicaciones más importantes de Fetch consiste en comunicarse con APIs REST. Actualmente la mayoría de aplicaciones web, móviles y de escritorio utilizan este tipo de servicios para intercambiar información.
Una API (Application Programming Interface) es un conjunto de reglas que permite que dos programas se comuniquen entre sí. Una API define:
- Qué operaciones pueden realizarse.
- Cómo deben enviarse los datos.
- Qué respuestas devolverá el servidor.
53.1 Recursos API
Una API suele organizar la información en recursos, y cada uno de esos recursos cuenta con una URL específica. Tomando como ejemplo la API pública {JSON} Placeholder con datos fake, los recursos serían:
- /users: obtener todos los usuarios.
- /users/1: obtener el usuario con ID 1.
- /posts: obtener todos los posts
- /posts/1/comments: obtener los comentarios del post con ID 1.
- /comments: obtener todos los comentarios.
Normalmente las API públicas suelen tener un interfaz web con los modelos de datos y los métodos/recursos que podemos consumir. Lo habitual es que hagan uso de la especificación OpenAPI. Por ejemplo:
- Swagger: interfaz web. Podemos ver un ejemplo en OpenData Euskadi.
- Scalar: interfaz web más moderno.
53.2 ¿Qué significa REST?
REST (Representational State Transfer) es un estilo de diseño para crear servicios web. En una API REST cada recurso dispone de una dirección (URL) y se manipula mediante los métodos HTTP.
Los métodos HTTP que se utilizan para comunicarse con una API son los siguientes, aunque hay alguno más:
| Método | URL | Acción |
|---|---|---|
| GET | /users | Devuelve todos los usuarios. |
| GET | /users/15 | Devuelve el usuario con ID 15. |
| POST | /users | Crea un nuevo usuario. |
| PUT | /users/15 | Sustituye un recurso existente. |
| PATCH | /users/15 | Modifica parcialmente un recurso. |
| DELETE | /users/15 | Eliminar dicho usuario. |
A Junio de 2026 existe una propuesta para un nuevo método QUERY especificado en RFC 10008. Hay un vídeo explicativo de midudev explicando cómo funcionará en el futuro.
54 Consumo de una API REST completa
Hasta este momento hemos aprendido a realizar peticiones HTTP mediante fetch() y a procesar las respuestas utilizando promesas y async / await. Vamos a desarrollar un ejemplo completo que reúne los conceptos vistos previamente. Para ello utilizaremos la API pública {JSON} Placeholder y construiremos una pequeña aplicación capaz de:
- Obtener información del servidor.
- Mostrar los datos en una tabla HTML.
- Crear nuevos registros.
- Modificar registros existentes.
- Eliminar registros.
54.1 Obtener datos con GET
Vamos a descargar la lista de usuarios, para ello se realiza una petición fetch() como se ha visto previamente:
Obtener usuarios
async function cargarUsuarios() {
try {
const respuesta = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
if (!respuesta.ok) {
throw new Error("Error al obtener los usuarios.");
}
const usuarios = await respuesta.json();
console.log(usuarios);
}
catch (error) {
console.error(error);
}
}
54.2 Mostrar datos
Una vez obtenidos los usuarios podemos recorrer el array e insertarlos dentro de una tabla HTML.
Tabla HTML
<table>
<thead>
<tr>
<th>Id</th>
<th>Nombre</th>
<th>Email</th>
</tr>
</thead>
<tbody id="usuarios">
</tbody>
</table>
Insertar datos en tabla
for (const usuario of usuarios) {
const fila = document.createElement("tr");
// TODO: cambiar innerHTML por createElement
fila.innerHTML = `
<td>${usuario.id}</td>
<td>${usuario.name}</td>
<td>${usuario.email}</td>
`;
tbody.appendChild(fila);
}
54.3 Crear un nuevo registro con POST
Para crear un nuevo usuario debemos usar el método POST. Los datos los podemos obtener a través de un formulario (convenientemente validado).
Crear un registro con POST
const usuario = {
name: "Alice",
email: "alice@example.com"
};
const respuesta = await fetch(
"https://jsonplaceholder.typicode.com/users",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(usuario)
}
);
A continuación se explica la diferencia de este método fetch() ya que es ligeramente diferente a lo visto anteriormente, ya que ahora recibe un segundo parámetro con opciones:
- URL: como se ha visto hasta ahora, hay que indicar la URL a la que realizaremos la petición.
- options: un objeto con distintas opciones:
- method: método utilizado cuando se realice la petición, en este caso POST.
- headers: cabeceras HTTP que se pueden enviar al realizar la petición.
- Content-Type: el tipo de contenido que se va a enviar. Normalmente al usar APIs se usa JSON.
- body: el contenido que se envía junto a la petición, en este caso los datos de un nuevo usuario.
Los objetos JavaScript no pueden enviarse directamente mediante HTTP, primero deben convertirse en una cadena JSON. Por ello está la función JSON.stringify() que convierte el objeto a formato JSON.
54.4 Modificar un registro (PUT / PATCH)
A la hora de modificar un registro de información podemos hacer uso de dos métodos:
- PUT: sustituye completamente el recurso.
- PATCH: permite modificar únicamente algunos campos.
Método PUT
await fetch(
"https://URL/users/3",
{
method: "PUT",
headers: {
"Content-Type":"application/json"
},
body: JSON.stringify({
id:3,
name:"María",
email:"maria@example.com",
// resto de campos
})
}
);
Método PATCH
await fetch(
"https://URL/users/3",
{
method: "PATCH",
headers: {
"Content-Type":"application/json"
},
body: JSON.stringify({
email:"nuevo@email.com"
})
}
);
{JSON} Placeholder no permite realizar modificaciones ni borrar datos, pero responderá correctamente a las peticiones.
54.5 Eliminar un registro con DELETE
Para eliminar un recurso utilizamos el método HTTP DELETE. Si la operación finaliza correctamente, el recurso habrá sido eliminado.
Método DELETE
await fetch(
"https://jsonplaceholder.typicode.com/users/3",
{
method:"DELETE"
}
);
En los últimos ejemplos no se ha utilizado try…catch ni se ha comprobado el estado de la petición para simplificarlos, pero deben usarse.
Crea una página que realice las siguientes peticiones fetch cuando sea necesario:
- Obtenga los usuarios y los inserte en una tabla.
- Cada fila tendrá una columna llamada “Acciones”, que tendrá dos botones:
- “Editar”: para editar los datos del usuario (abrirá un modal con un formulario).
- “Borrar”: para borrar el usuario concreto. Eliminará la fila.
- Botón “Añadir Usuario”: abre un modal con un formulario para crear usuario
- Tendrá los campos validados.
- Los mensajes del navegador serán modificados.
- Se añadirá al final de la tabla.
- Cada fila tendrá una columna llamada “Acciones”, que tendrá dos botones:
55 Herramientas de desarrollo para APIs REST
Hasta ahora hemos aprendido a realizar peticiones HTTP utilizando fetch(). Sin embargo, cuando una aplicación no funciona correctamente, resulta imprescindible conocer qué está ocurriendo entre el navegador y el servidor.
55.2 Herramientas para probar APIs
Durante el desarrollo de una aplicación no siempre resulta práctico realizar las pruebas desde el navegador o desde la propia aplicación que estamos realizando. En muchas ocasiones es conveniente utilizar herramientas que permitan enviar peticiones HTTP directamente a una API y analizar las respuestas obtenidas.
Estas aplicaciones facilitan el envío de peticiones, permiten añadir cabeceras HTTP, parámetros, autenticación y visualizar de forma clara las respuestas del servidor, así como guardar las peticiones en colecciones por proyectos/endpoints.
Entre las aplicaciones existentes podemos destacar Postman, Firecamp (versión web o escritorio), Bruno o Insomnia.

Desde una consola Linux, haciendo uso del comando curl, también podemos comprobar de manera rápida si el endpoint está funcionando:
Uso de curl en consola
ruben@vega:~$ curl -s http://localhost/api/posts
{"posts":[{"id":1,"titulo":"Primer post111","texto":"Este es...”}]}
Si queremos tener un resultado más visual, podremos hacer uso del comando “jq”, que deberemos instalarlo. De esta manera, podremos hacer:
Uso de jq para formatear la salida
ruben@vega:~$ curl -s http://localhost/api/posts | jq
{
"posts": [
{
"id": 1,
"titulo": "Primer post",
"texto": "Este es el texto del primer post",
"publicado": 1,
"deleted_at": null,
"created_at": "2024-10-29T17:09:56.000000Z",
"updated_at": "2024-10-29T18:09:56.000000Z"
}
]
}
Aunque estas herramientas son muy útiles para desarrollar y depurar APIs, conviene recordar que no sustituyen a fetch(). Su objetivo es comprobar que un servicio web funciona correctamente antes de integrarlo en nuestra aplicación JavaScript. Una vez verificada la API con estas herramientas, el siguiente paso consiste en acceder a ella desde nuestro código utilizando la Fetch API.
Almacenamiento de datos en el navegador
57 Web Storage API
La Web Storage API es una interfaz proporcionada por todos los navegadores modernos que permite almacenar información mediante pares clave-valor. Está formada por dos objetos:
- localStorage
- sessionStorage
El funcionamiento de ambas es muy parecido, siendo la diferencia principal el tiempo durante el que permanecen almacenados los datos.
57.1 Sistemas clave-valor
Un sistema clave-valor funciona a través de:
- clave:: es la manera de identificar el dato.
- valor:: contiene la información asociada, la información que nos interesa.
Hoy en día es un sistema muy utilizado, incluso en sistemas de bases de datos NoSQL. Por poner sólo unos ejemplos aparte de los sistemas que veremos después:
- Redis: Base de datos en memoria muy rápida, utilizada como caché, almacenamiento de sesiones y colas de mensajes.
- Valkey: Fork de Redis por discrepancias por el cambio de licencia realizado en 2024.
- Memcached: Sistema de almacenamiento en memoria basado en clave-valor, empleado principalmente para acelerar aplicaciones web almacenando datos temporales.
- DragonFlyDB: sistema compatible con las anteriores.
57.2 Datos que pueden almacenar
Tanto localStorage como sessionStorage almacenan únicamente cadenas de texto. Si queremos guardar números, arrays u objetos será necesario convertirlos previamente a formato JSON.
En la siguiente tabla podemos ver un resumen de las diferencias entre ambas
57.3 Acceso desde “mismo origen”
Los datos almacenados sólo son accesibles desde páginas que pertenezcan al mismo origen (origin). El origen está formado por:
- Protocolo (http o https)
- Dominio
- Puerto
Por ejemplo, un dato almacenado desde “https://www.ejemplo.com” no podrá acceder a los datos almacenados por “https://midominio.com”. Este comportamiento forma parte de la política de seguridad del navegador.
58 Uso de localStorage
La función localStorage permite almacenar información de forma permanente en el navegador. Los datos permanecerán disponibles incluso después de cerrar el navegador o apagar el ordenador. Solo desaparecerán cuando:
- El usuario los elimine manualmente.
- El programa los elimine.
- Se limpien los datos del navegador.
58.1 Guardar información
Para guardar un dato debemos usar la función .setItem, que recibe dos parámetros: la clave y el valor. El valor siempre se guardará como texto, aunque sea un número.
Guardar información con localStorage
localStorage.setItem("nombre", "Alice");
localStorage.setItem("edad", 20); // "20"
Si queremos guardar un objeto deberemos guardarlo en formato JSON con JSON.stringify(), porque de no ser así, no se guardarán los datos:
Guardar información con localStorage
const alumno = {
nombre: "Bob",
edad: 20
};
// Esto guardará "[object Object]"
localStorage.setItem("alumno", alumno);
// forma correcta
localStorage.setItem("alumno", JSON.stringify(alumno));
Si queremos guardar un objeto debemos guardarlo en formato JSON usando JSON.stringify().
Si volvemos a usar la función con la misma clave, sobrescribirá el dato existente.
Si usamos la misma clave sobreescribirá el dato existente.
Si ahora vamos a la pestaña “Application” o “Almacenamiento” dentro de las herramientas del desarrollador del navegador, veremos que tenemos una sección de almacenamiento local con el valor:
58.2 Leer información
Para leer la información es necesario conocer la clave:
Leer información con localStorage
localStorage.getItem("nombre");
// para un objeto
const texto = localStorage.getItem("alumno");
const alumno = JSON.parse(texto);
console.log(alumno.nombre);
Si queremos leer y obtener un objeto tenemos que parsear el texto JSON guardado previamente. Para ello se usa la función JSON.parse() que nos devolverá un objeto. Después ya podremos usar el objeto.
Para obtener un objeto debemos usar JSON.parse() con el texto leído.
58.3 Eliminar información
De nuevo, para borrar un dato necesitamos conocer la clave.
Borrar información con localStorage
localStorage.removeItem("nombre");
Si queremos eliminar toda información tenemos la función clear(). Sólo borrará la información para ese origen, pero debe usarse con cuidado.
Borrar información con localStorage
localStorage.clear();
clear() sólo borra información para ese origen, pero hay que usarlo con cuidado.
58.4 Obtener número de elementos
Podemos obtener cuántos elementos tenemos guardados para el origen en el que nos encontramos:
Obtener número de elementos localStorage
localStorage.length;
58.5 Obtener claves
Sabiendo el número de elementos podemos recorrer las claves que tenemos guardadas, u obtener el nombre de ellas junto con el valor.
Obtener claves de localStorage
console.log(localStorage.key(0));
for (let i = 0; i < localStorage.length; i++) {
const clave = localStorage.key(i);
const valor = localStorage.getItem(clave);
console.log(clave, valor);
}
Usa las funciones vistas previamente de localStorage para guardar y obtener datos y objetos.
59 Uso de sessionStorage
sessionStorage funciona prácticamente igual que localStorage, siendo la principal diferencia que la información únicamente permanece disponible durante la sesión actual del navegador. Cuando la pestaña o ventana se cierra, todos los datos desaparecen automáticamente.
Con sessionStorage los datos desaparecen al cerrar la pestaña o ventana.
Podemos usar sessionStorage para:
- Datos de un asistente de varios pasos.
- Información temporal durante una compra.
- Resultados de una búsqueda.
- Filtros temporales.
- Datos intermedios de un formulario.
- Preferencias que sólo deben mantenerse durante la sesión.
59.1 Guardar información
Funciona igual que lo visto previamente.
Guardar dato con sessionStorage
sessionStorage.setItem("pagina","inicio");
59.2 Leer información
Para leer la información es necesario conocer la clave:
Leer información con sessionStorage
sessionStorage.getItem("nombre");
59.3 Eliminar información
De nuevo, para borrar un dato necesitamos conocer la clave.
Borrar información con sessionStorage
sessionStorage.removeItem("nombre");
// borrar todos
sessionStorage.clear();
61 Comparativa entre localStorage, sessionStorage y cookies
Tal como se ha visto, los navegadores modernos ofrecen varios mecanismos para almacenar información, y cada uno de ellos presenta ventajas e inconvenientes. Conocer sus diferencias permite elegir la solución más adecuada en cada situación.
Sirva la siguiente tabla como resumen:
| localStorage | sessionStorage | Cookies | |
|---|---|---|---|
| Persistencia | Permanente | Solo durante la sesión | Configurable |
| Tamaño aproximado | 5-10 MB | 5-10 MB | 4 KB |
| Se envía automáticamente al servidor | No | No | Sí |
| Accesible desde JavaScript | Sí | Sí | Sí (salvo cookies HttpOnly) |
| Compartido entre pestañas | Sí | No | Sí |
62 Seguridad
Ninguno de estos mecanismos debe utilizarse para almacenar información confidencial. Por lo que nunca debería usarse para almacenar datos del estilo:
- Contraseñas.
- Números de tarjetas bancarias.
- Información personal sensible.
- Claves privadas.
Por este motivo, las aplicaciones profesionales almacenan la información realmente importante en el servidor y utilizan mecanismos adicionales de protección.
Ya se ha comentado previamente que los navegadores aplican la Same-Origin Policy (SOP), una política de seguridad que impide que una página web acceda directamente a los datos almacenados por otra página de un dominio diferente.No obstante, esta protección no hace que los datos sean completamente inaccesibles. Si el equipo del usuario resulta infectado por un programa malicioso, una extensión del navegador obtiene permisos excesivos o un atacante consigue ejecutar código dentro del propio sitio web (por ejemplo, mediante un ataque XSS), podría acceder a la información almacenada por ese dominio.
Noticias en los últimos años al respecto usando session hijacking: