Saltar al contenido

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.

javascript
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 tipo

En 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.

TipoQué guarda y qué conviene recordar
NumberNúmeros enteros y decimales. Incluye los valores especiales Infinity y NaN (Not a Number)
StringCadenas, con métodos como slice, replace, repeat y la propiedad length. Se concatenan con el signo más
Booleantrue o false
ArrayColección ordenada, con índice desde 0
ObjectConjunto de pares clave y valor
nullAusencia de valor asignada a propósito
undefinedVariable 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étodoQué hace¿Modifica el original?
pushAñade un elemento al final
popSaca el último elemento y lo devuelve
spliceAñade, borra o reemplaza elementos en una posición dada
forEachRecorre el array ejecutando una función por elemento; no devuelve nadaNo
mapDevuelve un array nuevo con el resultado de aplicar una función a cada elementoNo
filterDevuelve un array nuevo solo con los elementos que cumplen una condiciónNo
reduceRecorre el array acumulando y devuelve un único valorNo
sliceDevuelve una copia del rango indicadoNo
javascript
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.

javascript
// 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 llamada

Ese 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ónMétodo o propiedad
Buscar por identificadordocument.getElementById(...) devuelve un elemento
Buscar por etiqueta o por clasedocument.getElementsByTagName(...) y getElementsByClassName(...) devuelven listas de nodos
Buscar con sintaxis de selector CSSdocument.querySelector(...) devuelve el primero que cumpla; querySelectorAll(...) devuelve todos
Crear un nododocument.createElement(...): hay que crearlo antes de poder colgarlo
Colgar un nodo del árbolpadre.appendChild(...)
Llegar a la raízdocument.documentElement, que es la etiqueta <html>
Recorrer el árbolfirstChild y childNodes
javascript
// 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.

  1. Captura: el evento se recoge en la raíz del documento y va bajando hasta el elemento que lo generó.
  2. Destino (target): el evento llega al elemento concreto que lo ha generado y se gestiona allí.
  3. 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 propiedadQué 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.targetEl objeto original que provocó el evento
event.currentTargetEl objeto en el que se está tratando el evento en este momento
javascript
// 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

FamiliaOperadoresDetalle 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
Unariosa++ a-- ++a --aDetrá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 objetotypeof instanceof in delete voidtypeof 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.

javascript
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" });  // true

Para 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