JavaScript y el DOM
El lenguaje que se ejecuta en el navegador: qué clase de lenguaje es JavaScript, sus variables y tipos, los arrays y sus métodos, las funciones y las clausuras, cómo se manipula el documento desde el DOM y cómo viajan los eventos por el árbol.
Qué clase de lenguaje es JavaScript
JavaScript es un lenguaje interpretado: no se compila a un ejecutable, sino que el motor del navegador lo lee y lo ejecuta directamente. Es dinámico, en el sentido de que el tipo de una variable se decide en tiempo de ejecución y puede cambiar. Y está orientado a eventos: el grueso del código no se ejecuta en orden de arriba abajo, sino cuando ocurre algo (un clic, una tecla, la llegada de una respuesta del servidor).
Sobre la orientación a objetos hay que ser preciso, porque los apuntes suelen decirlo mal. JavaScript sí es orientado a objetos, pero no basado en clases sino basado en prototipos: cada objeto puede tener otro objeto como prototipo del que hereda propiedades y métodos, y las funciones son objetos a los que se les añade la propiedad prototype. Desde ES6 existe además la palabra clave class, que es una forma de escribir más cómoda por encima de ese mismo mecanismo de prototipos.
- Funciones de primera clase: una función se puede guardar en una variable, pasarse como parámetro y devolverse desde otra función.
- Clausuras o closures: una función puede seguir accediendo a variables que estaban fuera de ella cuando se creó, aunque ya haya terminado el contexto donde nacieron.
- Expresiones regulares: el método test devuelve verdadero o falso, y exec extrae lo que encaja con el patrón.
- Promesas y clases, incorporadas en ES6.
- Funciones de array (forEach, map, filter) y funciones flecha, equivalentes a las lambdas de otros lenguajes.
Los objetos que el navegador pone a disposición del script forman una jerarquía: Navigator representa al navegador, Window a la ventana o pestaña, y Document al documento cargado en ella. Para las pruebas unitarias del código, las herramientas habituales son Jest, Mocha y Jasmine.
Decir que JavaScript no es orientado a objetos es falso. Lo correcto es que su orientación a objetos es por prototipos y no por clases, y que la sintaxis class de ES6 se apoya en esos prototipos.
Para el examen
Su orientación a objetos: por PROTOTIPOS, no por clases
La sintaxis class de ES6: se apoya en esos prototipos
Afirmación falsa típica: que JavaScript no es orientado a objetos
Variables, tipos de datos y comparación
Hay tres formas de declarar una variable y se diferencian en el ámbito y en si se puede reasignar. var tiene ámbito de función: existe en toda la función donde se declaró, aunque se haya escrito dentro de un if. let tiene ámbito de bloque: solo existe entre las llaves donde se declaró. const declara una constante, que no admite reasignación.
const NOTA_CORTE = 5; // no se puede reasignar
let aciertos = 18; // ámbito de bloque
var intentos = 3; // ámbito de función
if (aciertos >= 15) {
let mensaje = "Subtema superado"; // solo existe aquí dentro
var aviso = "Sigue así"; // existe en toda la función
}
console.log(typeof aciertos); // "number"
// Comparación: el error más repetido del lenguaje
console.log(1 == "1"); // true : compara valor y convierte tipos
console.log(1 === "1"); // false: compara valor Y tipoEn JavaScript no es obligatorio declarar una variable antes de usarla, aunque hacerlo es lo correcto. Su nombre solo puede contener letras, números y los símbolos dólar y guion bajo, y no puede empezar por un número. El operador typeof devuelve el tipo de un valor.
| Tipo | Qué guarda y qué conviene recordar |
|---|---|
| Number | Números enteros y decimales. Incluye los valores especiales Infinity y NaN (Not a Number) |
| String | Cadenas, con métodos como slice, replace, repeat y la propiedad length. Se concatenan con el signo más |
| Boolean | true o false |
| Array | Colección ordenada, con índice desde 0 |
| Object | Conjunto de pares clave y valor |
| null | Ausencia de valor asignada a propósito |
| undefined | Variable declarada a la que todavía no se ha dado ningún valor |
El doble igual compara el valor convirtiendo tipos, así que 1 == "1" es cierto. El triple igual compara valor y tipo, así que 1 === "1" es falso. En la práctica se usa siempre el triple.
Para el examen
Doble igual (==): compara convirtiendo tipos
Triple igual (===): compara valor y tipo
Cuál se usa: siempre el triple
Arrays y sus métodos
El array es la estructura de datos que más se usa en el cliente, porque casi todo lo que llega del servidor es una lista: los temas de un bloque, las preguntas de un test, los intentos de un alumno. Sus métodos se dividen en los que modifican el array original y los que devuelven uno nuevo, y esa diferencia es la que se pregunta.
| Método | Qué hace | ¿Modifica el original? |
|---|---|---|
| push | Añade un elemento al final | Sí |
| pop | Saca el último elemento y lo devuelve | Sí |
| splice | Añade, borra o reemplaza elementos en una posición dada | Sí |
| forEach | Recorre el array ejecutando una función por elemento; no devuelve nada | No |
| map | Devuelve un array nuevo con el resultado de aplicar una función a cada elemento | No |
| filter | Devuelve un array nuevo solo con los elementos que cumplen una condición | No |
| reduce | Recorre el array acumulando y devuelve un único valor | No |
| slice | Devuelve una copia del rango indicado | No |
const preguntas = [
{ id: "q1", nivel: 1, acertada: true },
{ id: "q2", nivel: 3, acertada: false },
{ id: "q3", nivel: 1, acertada: false },
];
// filter devuelve un array nuevo con las falladas
const falladas = preguntas.filter(p => !p.acertada);
// map transforma cada elemento en otra cosa
const idsFallados = falladas.map(p => p.id); // ["q2", "q3"]
// reduce resume el array en un único valor
const aciertos = preguntas.reduce((total, p) => total + (p.acertada ? 1 : 0), 0);
// forEach solo recorre: no devuelve nada
preguntas.forEach((p, indice) => console.log(indice, p.id));
// splice SÍ modifica el original: inserta sin borrar nada
idsFallados.splice(1, 0, "q7"); // ["q2", "q7", "q3"]map devuelve un array nuevo y forEach no devuelve nada. Encadenar algo detrás de un forEach es el fallo típico: no hay nada que encadenar.
Para el examen
map: devuelve un array nuevo
forEach: no devuelve nada
Fallo típico: encadenar algo detrás de un forEach
Funciones, funciones flecha y clausuras
En JavaScript las funciones son objetos, y de ahí sale casi todo lo que el lenguaje tiene de particular. Al ser objetos, cumplen las tres condiciones que definen a las funciones de primera clase: se pueden asignar a una variable, se pueden pasar como parámetro a otra función y se pueden devolver desde otra función.
De la segunda condición sale el patrón más frecuente del lenguaje: la función que se pasa a otra para que la llame cuando toque. Se la llama función de retorno, callback, manejador o escuchador según el contexto, pero es siempre la misma idea.
La función flecha es una forma abreviada de escribir una función, equivalente a las expresiones lambda de otros lenguajes. Si el cuerpo es una sola expresión, se puede omitir la palabra return y las llaves.
// Función clásica y su equivalente en flecha
function nota(aciertos) { return aciertos * 0.5; }
const notaFlecha = aciertos => aciertos * 0.5;
// CLAUSURA: contadorDeFallos recuerda la variable fallos
// aunque la función que la creó ya terminó
function crearContadorDeFallos() {
let fallos = 0; // vive fuera de la función interna
return function () {
fallos += 1; // y la interna sigue accediendo a ella
return fallos;
};
}
const contarFallo = crearContadorDeFallos();
contarFallo(); // 1
contarFallo(); // 2 <- no se reinicia en cada llamadaEse ejemplo es una clausura: una función que conserva acceso a variables de fuera de su propio cuerpo, las que existían donde se definió. Sirve para dotar a una función de un contexto privado con datos, que es la forma que tiene JavaScript de conseguir algo parecido a un atributo privado sin tener clases con visibilidad.
Para el examen
Qué son las funciones: objetos y de primera clase: se asignan, se pasan y se devuelven
De ahí sale: el callback
Clausura: la función que conserva acceso a las variables de donde se definió
El DOM: el documento como árbol de objetos
Cuando el navegador carga una página, no guarda el HTML como texto: lo convierte en un árbol de objetos llamado DOM, Document Object Model. Cada etiqueta es un objeto de ese árbol, y en el vocabulario del DOM a las etiquetas se las llama Element. Manipular ese árbol desde JavaScript es lo que permite crear nodos, modificarlos y eliminarlos sin recargar la página.
| Operación | Método o propiedad |
|---|---|
| Buscar por identificador | document.getElementById(...) devuelve un elemento |
| Buscar por etiqueta o por clase | document.getElementsByTagName(...) y getElementsByClassName(...) devuelven listas de nodos |
| Buscar con sintaxis de selector CSS | document.querySelector(...) devuelve el primero que cumpla; querySelectorAll(...) devuelve todos |
| Crear un nodo | document.createElement(...): hay que crearlo antes de poder colgarlo |
| Colgar un nodo del árbol | padre.appendChild(...) |
| Llegar a la raíz | document.documentElement, que es la etiqueta <html> |
| Recorrer el árbol | firstChild y childNodes |
// Buscar con sintaxis de selector CSS
const panel = document.querySelector("#panel-temario");
const pendientes = document.querySelectorAll("li[data-estado='pendiente']");
// Crear un nodo nuevo y colgarlo del árbol
const aviso = document.createElement("p");
aviso.textContent = `Te quedan ${pendientes.length} subtemas.`;
aviso.dataset.origen = "panel";
panel.appendChild(aviso);
// Registrar un manejador: la forma recomendada
const boton = document.querySelector("#corregir");
boton.addEventListener("click", function (evento) {
console.log("Corrigiendo el intento...");
});Hay dos formas de asociar código a un evento. La antigua consiste en escribir el nombre del manejador en un atributo HTML como onclick, o en asignar una función a la propiedad onclick del elemento; su límite es que solo admite un manejador, porque asignar otro pisa al anterior. La recomendada es addEventListener, que permite registrar tantos manejadores como haga falta sobre el mismo elemento y el mismo evento, y además controlar en qué fase se ejecutan.
querySelector devuelve un solo nodo y querySelectorAll devuelve una lista. Los métodos getElementsBy... en plural también devuelven listas; el único que devuelve un elemento suelto es getElementById.
Para el examen
querySelector: devuelve un nodo
querySelectorAll: devuelve una lista
Los getElementsBy en plural: devuelven listas
El único que devuelve un elemento suelto: getElementById
Eventos: fases, propagación y cancelación
Cuando el usuario hace clic en un botón que está dentro de una tarjeta que está dentro de un panel, el evento no ocurre solo en el botón: recorre el árbol entero en tres fases sucesivas. Entender ese recorrido es lo que permite escribir un solo manejador para muchos elementos.
- Captura: el evento se recoge en la raíz del documento y va bajando hasta el elemento que lo generó.
- Destino (target): el evento llega al elemento concreto que lo ha generado y se gestiona allí.
- Burbujeo o propagación: el evento vuelve a subir desde ese elemento hasta la raíz, pasando por todos sus padres.
El burbujeo es lo que hace posible la delegación de eventos: si hay que registrar cuándo se pincha en cualquiera de las trescientas preguntas de un test, no se ponen trescientos manejadores; se pone uno solo en el contenedor y se mira, cuando el evento sube, de dónde venía.
| Método o propiedad | Qué hace |
|---|---|
| stopPropagation() | Corta el burbujeo: el evento deja de subir. No puede detener la fase de captura |
| preventDefault() | Cancela el comportamiento por defecto del navegador, como seguir un enlace o enviar un formulario |
| event.target | El objeto original que provocó el evento |
| event.currentTarget | El objeto en el que se está tratando el evento en este momento |
// Delegación: un manejador para toda la lista de preguntas
const listaPreguntas = document.querySelector("#preguntas");
listaPreguntas.addEventListener("click", function (evento) {
// currentTarget es siempre la lista, porque es donde escuchamos
// target es la opción concreta que el alumno ha pulsado
const opcion = evento.target.closest("li");
if (!opcion) return;
evento.preventDefault(); // no seguimos el enlace de la opción
marcarRespuesta(opcion.dataset.pregunta, opcion.dataset.indice);
});- focus: el elemento recibe el foco, con el ratón o con el tabulador. blur: lo pierde.
- click: el usuario pulsa sobre el elemento.
- keypress: el usuario pulsa una tecla.
- input: el usuario escribe en una caja de texto.
- select: el usuario selecciona una parte del texto de un campo.
- change: cambia el valor de un desplegable, o el elemento pierde el foco después de haberse modificado.
event.target es quien lo provocó y event.currentTarget es quien lo está escuchando. En delegación de eventos casi nunca coinciden, y confundirlos es el fallo habitual.
Para el examen
event.target: quien provocó el evento
event.currentTarget: quien lo está escuchando
En delegación: casi nunca coinciden
Operadores de JavaScript
| Familia | Operadores | Detalle que se pregunta |
|---|---|---|
| Aritméticos | + - * / % ** | % es el resto de la división (módulo) y ** la exponenciación, equivalente a Math.pow(a, b) |
| Asignación | = += -= *= /= %= **= | a += b es exactamente a = a + b |
| Unarios | a++ a-- ++a --a | Detrás es post: usa el valor y luego lo cambia. Delante es pre: lo cambia y luego lo usa |
| Comparación | == === != !== > < >= <= | El doble igual convierte tipos, el triple no |
| Lógicos | ! && || | && y || evalúan en cortocircuito: si el primer operando ya decide el resultado, el segundo ni se evalúa |
| De tipo y objeto | typeof instanceof in delete void | typeof da el tipo de un valor, instanceof comprueba de qué constructor procede un objeto e in si una propiedad existe |
El cortocircuito de los operadores lógicos no es una curiosidad: es un idioma del lenguaje. Escribir alumno && alumno.curso devuelve undefined en vez de lanzar un error cuando alumno no existe, porque el segundo operando ni se llega a evaluar.
let intentos = 2;
console.log(intentos ** 2); // 4 : exponenciación
console.log(7 % 3); // 1 : resto de la división
let i = 5;
console.log(i++); // 5 : usa y luego incrementa
console.log(i); // 6
console.log(++i); // 7 : incrementa y luego usa
const alumno = null;
console.log(alumno && alumno.curso); // null, sin error: cortocircuito
console.log(typeof intentos); // "number"
console.log([] instanceof Array); // true
console.log("curso" in { curso: "largo" }); // truePara el examen
Cortocircuito: si el primer operando del Y lógico no existe, el segundo ni se evalúa
Qué devuelve entonces: undefined, en vez de lanzar un error