materials

Browser Object Model (BOM)

Introducción

En este tema veremos cómo acceder a objetos que nos permitan interactuar con el navegador (Browser Object Model, BOM).

Usando los objetos BOM podemos:

Al contrario que el DOM, el BOM no nació como un estándar sino como un conjunto de objetos que cada navegador fue añadiendo. Hoy en día la mayor parte está recogida en el estándar HTML y funciona igual en todos los navegadores, pero arrastra muchas propiedades antiguas que ya no sirven para nada o que devuelven siempre el mismo valor. Las iremos señalando.

Timers

Permiten ejecutar código en el futuro (cuando transcurran, como mínimo, los milisegundos indicados). Hay 2 tipos:

A ambas se les pueden pasar más parámetros tras los milisegundos y serán los parámetros que recibirá la función a ejecutar.

Ambas funciones devuelven un identificador que nos permitirá cancelar la ejecución del código usando:

Ejemplo:

const idTimeout = setTimeout(() => console.log('Timeout que se ejecuta al cabo de 1 seg.'), 1000);

let i = 1;
const idInterval = setInterval(() => {
  console.log('Interval cada 3 seg. Ejecución nº: ' + i++);
  if (i === 5) {
    clearInterval(idInterval);
    console.log('Fin de la ejecución del Interval');
  }
}, 3000);

EJERCICIO: Ejecuta en la consola cada una de esas funciones

En lugar de definir la función a ejecutar podemos pasar una función que ya exista:

function showMessage() {
  console.log('Timeout que se ejecuta al cabo de 1 seg.')
}

const idTimeout = setTimeout(showMessage, 1000);

Pero en ese caso hay que poner sólo el nombre de la función, sin (), ya que si los ponemos se ejecutaría la función en ese momento y a setTimeout le llegaría lo que devuelve (normalmente undefined).

Si necesitamos pasarle algún parámetro a la función lo añadiremos como parámetros de setTimeout o setInterval después del intervalo:

function showMessage(msg) {
  console.log(msg)
}

const idTimeout = setTimeout(showMessage, 1000, 'Timeout que se ejecuta al cabo de 1 seg.');

Aunque es más habitual (y más claro) usar una arrow function:

const idTimeout = setTimeout(() => showMessage('Timeout que se ejecuta al cabo de 1 seg.'), 1000);

Los timers no son exactos

Javascript en el navegador se ejecuta en un único hilo. Cuando se cumple el tiempo de un timer su función no se ejecuta en ese instante sino que se pone en una cola, y se ejecutará cuando el hilo quede libre (cuando termine el código que se esté ejecutando en ese momento). Esto tiene varias consecuencias:

Por tanto un timer sirve para decidir cuándo hacer algo, pero no para medir el tiempo. Si necesitamos saber cuánto tiempo ha pasado (un cronómetro, una cuenta atrás, …) no contaremos cuántas veces se ha ejecutado el interval sino que guardaremos el momento de inicio y calcularemos la diferencia con el momento actual:

// MAL: cuenta ejecuciones, no tiempo
let seconds = 60;
const id = setInterval(() => {
  seconds--;
  console.log(seconds);
  if (seconds === 0) clearInterval(id);
}, 1000);

// BIEN: el interval sólo sirve para refrescar; el tiempo se calcula
const end = Date.now() + 60 * 1000;
const id = setInterval(() => {
  const remaining = Math.max(0, Math.round((end - Date.now()) / 1000));
  console.log(remaining);
  if (remaining === 0) clearInterval(id);
}, 1000);

EJERCICIO: ejecuta en la consola el siguiente código, que muestra cada segundo cuántos ms han pasado realmente desde la ejecución anterior. Después, mientras se está ejecutando, ejecuta const t = Date.now(); while (Date.now() - t < 3000) {} (bloquea el hilo 3 segundos). ¿Qué observas? ¿Qué pasa si cambias a otra pestaña un rato y vuelves?

let last = Date.now();
const id = setInterval(() => {
  const now = Date.now();
  console.log(now - last);
  last = now;
}, 1000);
// para pararlo: clearInterval(id)

Errores habituales con los timers

let idInterval;

function start() {
  clearInterval(idInterval);   // si no hay ninguno no pasa nada
  idInterval = setInterval(tick, 1000);
}
class Counter {
  constructor() {
    this.value = 0;
  }
  increment() {
    this.value++;
    console.log(this.value);
  }
}

const counter = new Counter();
setInterval(counter.increment, 1000);         // MAL: this no es counter -> NaN
setInterval(() => counter.increment(), 1000); // BIEN

Objetos del BOM

Objeto window

Representa la ventana (o pestaña) del navegador y es el objeto global: todo lo que no pertenece a otro objeto pertenece a él. Por eso puede omitirse al llamar a sus propiedades y métodos, por ejemplo, setTimeout() es en realidad window.setTimeout() y location es window.location.

NOTA: las variables globales declaradas con var y las funciones globales se convierten en propiedades de window (las declaradas con let y const no). Es otro motivo para no usar var. Si estamos en un módulo (por ejemplo con Vite) nada de lo que declaremos es global y, si queremos que algo sea accesible desde la consola, tendremos que hacerlo explícitamente: window.miFuncion = miFuncion.

NOTA: en otros entornos (como NodeJS o Vitest) no existe window. Si necesitamos referirnos al objeto global sea cual sea el entorno podemos usar globalThis.

Sus principales propiedades y métodos son:

Otras propiedades que encontraréis en documentación antigua no funcionan o no hacen nada: .status (los navegadores ya no tienen barra de estado accesible), .home(), .back()/.forward() (se usa history), .find() (no estándar).

EJERCICIO: desde la consola obtén el tamaño útil de la ventana. Cambia el tamaño de la ventana del navegador (o abre las herramientas de desarrollador acopladas a un lado) y vuelve a obtenerlo. Haz scroll en una página larga y muestra cuánto te has desplazado. Haz que vuelva arriba suavemente.

Abrir y cerrar ventanas

Hay que tener en cuenta que:

const win = window.open('', 'saludo', 'width=500,height=200,left=100,top=200');
win.document.write('<h1>Hola</h1>');
win.moveBy(-100, 300);
win.close();

EJERCICIO: ejecuta desde la consola el código anterior instrucción por instrucción. Después:

Puedes ver un ejemplo de cómo abrir ventanas en este vídeo, aunque ten en cuenta lo anterior: algunas cosas que se ven ya no funcionan en los navegadores actuales.

Diálogos

Hay 3 métodos del objeto window que ya conocemos y que nos permiten abrir ventanas de diálogo con el usuario:

Hay que tener en cuenta que:

Por eso en aplicaciones reales no se usan (salvo quizá un confirm rápido antes de borrar algo) y se sustituyen por ventanas modales hechas con HTML. Y nunca se usa alert para depurar: para eso está console.log.

Cuidado al convertir lo que devuelve prompt a número:

const edad = Number(prompt('¿Cuántos años tienes?'));
// Si escribe 'abc' -> NaN
// Si deja el cuadro vacío -> Number('') es 0
// Si pulsa Cancelar -> Number(null) es 0

EJERCICIO: pide al usuario un número con prompt y muestra en la consola su doble. Asegúrate de que tu código distingue entre: el usuario cancela, deja el cuadro vacío, escribe algo que no es un número y escribe un número válido.

Objeto location

Contiene información sobre la URL actual y nos permite cambiarla. Sus principales propiedades son (para la URL https://www.ejemplo.com:8080/tienda/productos.html?cat=5&orden=precio#ofertas):

Propiedad Valor
.href la URL completa
.origin https://www.ejemplo.com:8080
.protocol https:
.host www.ejemplo.com:8080
.hostname www.ejemplo.com
.port 8080 (vacío si es el puerto por defecto)
.pathname /tienda/productos.html
.search ?cat=5&orden=precio
.hash #ofertas

Y sus métodos:

Si cambiamos sólo el hash (location.hash = '#seccion2') la página no se recarga, sólo se desplaza a ese ancla y se añade una entrada al historial.

EJERCICIO: Ejecuta en la consola

Los objetos URL y URLSearchParams

La propiedad search nos devuelve la cadena de búsqueda completa (?cat=5&orden=precio) pero normalmente lo que queremos es el valor de uno de sus parámetros. No debemos extraerlo a mano con split o con expresiones regulares porque es fácil equivocarse (¿y si el parámetro no es el primero? ¿y si el valor tiene caracteres codificados como %20?). Para eso existe URLSearchParams:

// location.search vale '?cat=5&orden=precio'
const params = new URLSearchParams(location.search);
params.get('cat');      // '5' (siempre devuelve un string)
params.get('pagina');   // null (no existe)
params.has('orden');    // true

Y si necesitamos trabajar con una URL que no es la actual, o construir una, tenemos el objeto URL, que tiene las mismas propiedades que location y además una propiedad searchParams:

const url = new URL('https://www.ejemplo.com/buscar');
url.searchParams.set('q', 'libros de javascript');
url.searchParams.set('pagina', 2);
url.href;   // 'https://www.ejemplo.com/buscar?q=libros+de+javascript&pagina=2'

Estos objetos los usaremos mucho cuando hagamos peticiones a un servidor.

EJERCICIO: haz una función getParam(nombre, porDefecto) que devuelva el valor del parámetro indicado de la URL actual o el valor por defecto si no existe. Pruébala abriendo tu página con distintos parámetros (?nombre=Ana&edad=20, ?edad=20&nombre=Ana, ?nombre=Ana%20María, …)

Objeto history

Permite acceder al historial de páginas visitadas en esta pestaña y navegar por él (por privacidad no podemos ver qué páginas son, sólo movernos por él):

Además permite añadir o modificar entradas del historial sin cargar ninguna página:

Cuando el usuario pulsa ‘Atrás’ o ‘Adelante’ sobre una entrada creada así, la página no se recarga sino que se produce el evento popstate. Esto es la base de las SPA (Single Page Applications): aplicaciones con una sola página HTML en las que el código cambia el contenido y la URL sin recargar, y en las que los botones del navegador siguen funcionando. Lo veremos en el tema de SPA y lo usaremos (a través del router) en Vue.

EJERCICIO: desde la consola:

Objeto navigator

Nos da información sobre el navegador y el sistema en que se ejecuta:

Otras propiedades que veréis en documentación antigua están obsoletas y devuelven siempre lo mismo en todos los navegadores para no dar información al usuario: .appName (siempre 'Netscape'), .appCodeName (siempre 'Mozilla'), .product (siempre 'Gecko'), .appVersion y .platform (obsoleta, aunque aún devuelve algo).

No debemos usar navigator para averiguar qué navegador es y hacer cosas distintas según el que sea. Si queremos saber si podemos usar una característica, comprobamos directamente si existe (feature detection):

// MAL
if (navigator.userAgent.includes('Chrome')) { ... }

// BIEN
if ('geolocation' in navigator) { ... }
if (navigator.clipboard) { ... }

También incluye objetos con sus propias API para poder interactuar con el sistema:

Muchas de estas API sólo funcionan en contextos seguros y devuelven promesas, que veremos en el tema de Ajax.

EJERCICIO: desde la consola muestra el idioma preferido del navegador y todos los que acepta. Cambia en la configuración del navegador el orden de los idiomas y vuelve a mirarlos. Haz una función que devuelva 'es', 'ca' o 'en' según el primero de los idiomas del usuario que esté entre esos tres (y 'en' si no hay ninguno).

Objeto screen

Nos da información sobre la pantalla:

Para decidir cómo mostrar nuestra página no usaremos el tamaño de la pantalla sino el de la ventana (innerWidth), y normalmente ni siquiera eso: es trabajo del CSS (media queries).

EJERCICIO: obtén desde la consola las propiedades width/height y availWidth/availHeight del objeto screen. Compáralas con las propiedades innerWidth/innerHeight y outerWidth/outerHeight de window y con devicePixelRatio. Haz zoom en la página (Ctrl +) y vuelve a mirarlas todas. ¿Cuáles cambian?

Contextos seguros

Algunas API del navegador sólo están disponibles en contextos seguros (secure contexts), es decir, en páginas servidas por https o desde localhost. En el resto esas propiedades no existen (por ejemplo navigator.clipboard será undefined). Entre ellas están el portapapeles, la geolocalización, la cámara y el micrófono o los service workers.

Mientras desarrollamos con Vite estamos en localhost, por lo que funcionan, pero si abrimos la página desde la IP del equipo (por ejemplo desde el móvil) o la publicamos en un servidor sin https dejarán de funcionar. Podemos comprobarlo con window.isSecureContext.