BOM (Browser Object Model)
¿Qué es el BOM?
El BOM (Browser Object Model) es el conjunto de objetos JavaScript que proporcionan acceso a las funcionalidades del navegador más allá del documento HTML. Mientras el DOM se enfoca en manipular el contenido de la página, el BOM te permite controlar el navegador mismo: ventanas, historial, ubicación, almacenamiento y más.
¿Para qué se utiliza el BOM?
El BOM es utilizado por desarrolladores para:
- Controlar ventanas y pestañas del navegador (abrir, cerrar, redimensionar).
- Navegar por el historial del navegador (back, forward, go).
- Manipular la URL actual sin recargar la página.
- Detectar información del navegador y sistema operativo.
- Gestionar cookies y almacenamiento local.
- Mostrar alertas, confirmaciones y prompts al usuario.
- Medir el rendimiento y tiempos de carga de páginas.
¿Cómo funciona?
El BOM funciona como el panel de control del navegador - te da acceso a todas las palancas, botones y configuraciones que controlan cómo funciona la ventana del navegador. Es como tener los controles de la cabina de un avión, pero para tu aplicación web.
El objeto Window: la raíz del BOM
window es el objeto global que representa la ventana del navegador:
// window es el objeto global
console.log(window); // Objeto Window completo
// En el navegador, todas las variables globales son propiedades de window
var globalVar = 'Soy global';
console.log(window.globalVar); // 'Soy global'
// Funciones globales también son métodos de window
function globalFunction() {
console.log('Función global');
}
window.globalFunction(); // 'Función global'
// window también contiene el objeto document (DOM)
console.log(window.document === document); // true
// Propiedades básicas de window
console.log('Ancho interno:', window.innerWidth);
console.log('Alto interno:', window.innerHeight);
console.log('Ancho externo:', window.outerWidth);
console.log('Alto externo:', window.outerHeight);
console.log('Posición X:', window.screenX);
console.log('Posición Y:', window.screenY);
Diálogos del navegador
// Alert - Mostrar mensaje simple
window.alert('¡Hola! Este es un alert');
// Confirm - Obtener confirmación del usuario
const userConfirmed = window.confirm('¿Estás seguro?');
if (userConfirmed) {
console.log('Usuario confirmó');
} else {
console.log('Usuario canceló');
}
// Prompt - Solicitar entrada de texto
const userName = window.prompt('¿Cuál es tu nombre?', 'Visitante');
if (userName !== null) {
console.log('Hola,', userName);
} else {
console.log('Usuario canceló el prompt');
}
// Clase helper para diálogos personalizados
class CustomDialog {
static alert(message, title = 'Alerta') {
return new Promise((resolve) => {
const dialog = document.createElement('div');
dialog.className = 'custom-dialog';
dialog.innerHTML = `
<div class="dialog-overlay"></div>
<div class="dialog-content">
<h3>${title}</h3>
<p>${message}</p>
<button class="btn-ok">Aceptar</button>
</div>
`;
document.body.appendChild(dialog);
dialog.querySelector('.btn-ok').addEventListener('click', () => {
document.body.removeChild(dialog);
resolve();
});
});
}
static confirm(message, title = 'Confirmar') {
return new Promise((resolve) => {
const dialog = document.createElement('div');
dialog.className = 'custom-dialog';
dialog.innerHTML = `
<div class="dialog-overlay"></div>
<div class="dialog-content">
<h3>${title}</h3>
<p>${message}</p>
<button class="btn-cancel">Cancelar</button>
<button class="btn-ok">Aceptar</button>
</div>
`;
document.body.appendChild(dialog);
dialog.querySelector('.btn-ok').addEventListener('click', () => {
document.body.removeChild(dialog);
resolve(true);
});
dialog.querySelector('.btn-cancel').addEventListener('click', () => {
document.body.removeChild(dialog);
resolve(false);
});
});
}
static async prompt(message, defaultValue = '', title = 'Entrada') {
return new Promise((resolve) => {
const dialog = document.createElement('div');
dialog.className = 'custom-dialog';
dialog.innerHTML = `
<div class="dialog-overlay"></div>
<div class="dialog-content">
<h3>${title}</h3>
<p>${message}</p>
<input type="text" value="${defaultValue}" />
<button class="btn-cancel">Cancelar</button>
<button class="btn-ok">Aceptar</button>
</div>
`;
document.body.appendChild(dialog);
const input = dialog.querySelector('input');
input.focus();
dialog.querySelector('.btn-ok').addEventListener('click', () => {
const value = input.value;
document.body.removeChild(dialog);
resolve(value);
});
dialog.querySelector('.btn-cancel').addEventListener('click', () => {
document.body.removeChild(dialog);
resolve(null);
});
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const value = input.value;
document.body.removeChild(dialog);
resolve(value);
}
});
});
}
}
// Uso de diálogos personalizados
// await CustomDialog.alert('Operación exitosa', 'Éxito');
// const confirmed = await CustomDialog.confirm('¿Continuar?', 'Confirmación');
// const name = await CustomDialog.prompt('Tu nombre:', 'Anónimo', 'Registro');
Window.location - Control de URLs
// Objeto location contiene información sobre la URL actual
console.log('URL completa:', window.location.href);
console.log('Protocolo:', window.location.protocol); // 'https:'
console.log('Host:', window.location.host); // 'ejemplo.com:443'
console.log('Hostname:', window.location.hostname); // 'ejemplo.com'
console.log('Puerto:', window.location.port); // '443'
console.log('Pathname:', window.location.pathname); // '/blog/articulo'
console.log('Search:', window.location.search); // '?id=123&cat=tech'
console.log('Hash:', window.location.hash); // '#seccion'
// Clase para manipular location de forma segura
class LocationManager {
// Obtener parámetros de query string
static getParams() {
return Object.fromEntries(new URLSearchParams(window.location.search));
}
// Obtener parámetro específico
static getParam(name, defaultValue = null) {
const params = new URLSearchParams(window.location.search);
return params.get(name) || defaultValue;
}
// Navegar a nueva URL
static navigate(url) {
window.location.href = url;
}
// Recargar página
static reload(forceReload = false) {
window.location.reload(forceReload);
}
// Reemplazar URL sin agregar al historial
static replace(url) {
window.location.replace(url);
}
// Agregar/modificar parámetros de query sin recargar
static updateParams(params) {
const url = new URL(window.location.href);
Object.entries(params).forEach(([key, value]) => {
if (value === null || value === undefined) {
url.searchParams.delete(key);
} else {
url.searchParams.set(key, value);
}
});
window.history.replaceState({}, '', url.toString());
}
// Eliminar parámetro específico
static removeParam(name) {
const url = new URL(window.location.href);
url.searchParams.delete(name);
window.history.replaceState({}, '', url.toString());
}
// Obtener ruta relativa
static getRelativePath() {
return window.location.pathname + window.location.search + window.location.hash;
}
// Verificar si es HTTPS
static isSecure() {
return window.location.protocol === 'https:';
}
// Verificar si es localhost
static isLocalhost() {
return ['localhost', '127.0.0.1', '::1'].includes(window.location.hostname);
}
}
// Ejemplos de uso
console.log('Parámetros:', LocationManager.getParams());
console.log('Usuario ID:', LocationManager.getParam('userId', 'guest'));
// Actualizar parámetros sin recargar
LocationManager.updateParams({ page: 2, sort: 'name' });
// Navegar a otra página
// LocationManager.navigate('/dashboard');
// Recargar forzando desde servidor
// LocationManager.reload(true);
Window.history - Navegación por historial
// Navegar por el historial
window.history.back(); // Ir atrás (equivale al botón "Atrás")
window.history.forward(); // Ir adelante
window.history.go(-2); // Ir 2 páginas atrás
window.history.go(1); // Ir 1 página adelante
// Cantidad de páginas en el historial
console.log('Páginas en historial:', window.history.length);
// History API para SPAs (Single Page Applications)
class HistoryManager {
constructor() {
this.setupPopstateListener();
}
// Agregar nueva entrada al historial
push(url, state = {}) {
window.history.pushState(state, '', url);
this.triggerRouteChange();
}
// Reemplazar entrada actual del historial
replace(url, state = {}) {
window.history.replaceState(state, '', url);
this.triggerRouteChange();
}
// Obtener estado actual
getState() {
return window.history.state;
}
// Navegar atrás
back() {
window.history.back();
}
// Navegar adelante
forward() {
window.history.forward();
}
// Escuchar cambios de ruta
setupPopstateListener() {
window.addEventListener('popstate', (event) => {
console.log('Estado del historial:', event.state);
this.triggerRouteChange();
});
}
// Evento personalizado para cambios de ruta
triggerRouteChange() {
const event = new CustomEvent('routechange', {
detail: {
path: window.location.pathname,
search: window.location.search,
hash: window.location.hash,
state: window.history.state,
},
});
window.dispatchEvent(event);
}
// Interceptar clics en enlaces
interceptLinks() {
document.addEventListener('click', (event) => {
const link = event.target.closest('a[href]');
if (link && this.shouldIntercept(link)) {
event.preventDefault();
const href = link.getAttribute('href');
this.push(href);
}
});
}
shouldIntercept(link) {
const href = link.getAttribute('href');
// No interceptar enlaces externos
if (href.startsWith('http') && !href.startsWith(window.location.origin)) {
return false;
}
// No interceptar si tiene target
if (link.target) {
return false;
}
// No interceptar download links
if (link.hasAttribute('download')) {
return false;
}
return true;
}
}
// Uso del HistoryManager
const historyManager = new HistoryManager();
// Navegar programáticamente
historyManager.push('/nueva-pagina', { userId: 123 });
// Escuchar cambios de ruta
window.addEventListener('routechange', (event) => {
console.log('Ruta cambió a:', event.detail.path);
console.log('Estado:', event.detail.state);
});
// Interceptar todos los enlaces
historyManager.interceptLinks();
Window.navigator - Información del navegador
// Información básica del navegador
console.log('User Agent:', window.navigator.userAgent);
console.log('Lenguaje:', window.navigator.language);
console.log('Lenguajes:', window.navigator.languages);
console.log('Online:', window.navigator.onLine);
console.log('Plataforma:', window.navigator.platform);
console.log('Cookies habilitadas:', window.navigator.cookieEnabled);
// Clase para detectar características del navegador
class BrowserDetector {
// Detectar navegador específico
static getBrowser() {
const ua = navigator.userAgent;
if (ua.includes('Firefox/')) {
return 'Firefox';
} else if (ua.includes('Edg/')) {
return 'Edge';
} else if (ua.includes('Chrome/')) {
return 'Chrome';
} else if (ua.includes('Safari/') && !ua.includes('Chrome')) {
return 'Safari';
} else if (ua.includes('Opera/') || ua.includes('OPR/')) {
return 'Opera';
}
return 'Unknown';
}
// Detectar sistema operativo
static getOS() {
const ua = navigator.userAgent;
const platform = navigator.platform;
if (ua.includes('Win')) return 'Windows';
if (ua.includes('Mac')) return 'macOS';
if (ua.includes('Linux')) return 'Linux';
if (ua.includes('Android')) return 'Android';
if (ua.includes('iOS') || platform.includes('iPhone') || platform.includes('iPad')) {
return 'iOS';
}
return 'Unknown';
}
// Detectar si es dispositivo móvil
static isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
}
// Detectar si es tablet
static isTablet() {
const ua = navigator.userAgent;
return /(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua);
}
// Detectar si es desktop
static isDesktop() {
return !this.isMobile() && !this.isTablet();
}
// Detectar soporte de características
static supports(feature) {
const features = {
touch: 'ontouchstart' in window,
serviceWorker: 'serviceWorker' in navigator,
geolocation: 'geolocation' in navigator,
webgl: !!document.createElement('canvas').getContext('webgl'),
websocket: 'WebSocket' in window,
localStorage: (() => {
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
return true;
} catch (e) {
return false;
}
})(),
indexedDB: 'indexedDB' in window,
notifications: 'Notification' in window,
camera: 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices,
};
return features[feature] || false;
}
// Información completa del dispositivo
static getDeviceInfo() {
return {
browser: this.getBrowser(),
os: this.getOS(),
deviceType: this.isMobile() ? 'mobile' : this.isTablet() ? 'tablet' : 'desktop',
language: navigator.language,
online: navigator.onLine,
cookiesEnabled: navigator.cookieEnabled,
screenResolution: `${window.screen.width}x${window.screen.height}`,
viewportSize: `${window.innerWidth}x${window.innerHeight}`,
colorDepth: window.screen.colorDepth,
pixelRatio: window.devicePixelRatio || 1,
};
}
}
// Uso del detector
console.log('Navegador:', BrowserDetector.getBrowser());
console.log('Sistema:', BrowserDetector.getOS());
console.log('Es móvil:', BrowserDetector.isMobile());
console.log('Soporta touch:', BrowserDetector.supports('touch'));
console.log('Info completa:', BrowserDetector.getDeviceInfo());
// Detectar estado de conexión
window.addEventListener('online', () => {
console.log('Conexión restaurada');
});
window.addEventListener('offline', () => {
console.log('Sin conexión');
});
Geolocalización
// API de geolocalización
class GeolocationManager {
// Obtener posición actual
static getCurrentPosition(options = {}) {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('Geolocalización no soportada'));
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
altitudeAccuracy: position.coords.altitudeAccuracy,
heading: position.coords.heading,
speed: position.coords.speed,
timestamp: position.timestamp,
});
},
(error) => {
reject(new Error(this.getErrorMessage(error)));
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0,
...options,
}
);
});
}
// Observar cambios de posición
static watchPosition(callback, errorCallback, options = {}) {
if (!navigator.geolocation) {
errorCallback(new Error('Geolocalización no soportada'));
return null;
}
const watchId = navigator.geolocation.watchPosition(
(position) => {
callback({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
timestamp: position.timestamp,
});
},
(error) => {
errorCallback(new Error(this.getErrorMessage(error)));
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0,
...options,
}
);
return watchId;
}
// Detener observación
static clearWatch(watchId) {
if (watchId !== null) {
navigator.geolocation.clearWatch(watchId);
}
}
// Obtener mensaje de error legible
static getErrorMessage(error) {
switch (error.code) {
case error.PERMISSION_DENIED:
return 'Permiso denegado por el usuario';
case error.POSITION_UNAVAILABLE:
return 'Posición no disponible';
case error.TIMEOUT:
return 'Tiempo de espera agotado';
default:
return 'Error desconocido';
}
}
// Calcular distancia entre dos puntos (fórmula de Haversine)
static calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Radio de la Tierra en km
const dLat = this.toRad(lat2 - lat1);
const dLon = this.toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRad(lat1)) *
Math.cos(this.toRad(lat2)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distancia en km
}
static toRad(degrees) {
return (degrees * Math.PI) / 180;
}
}
// Uso de geolocalización
async function getUserLocation() {
try {
const position = await GeolocationManager.getCurrentPosition();
console.log('Ubicación actual:', position);
// Calcular distancia a un punto específico
const distanceToMadrid = GeolocationManager.calculateDistance(
position.latitude,
position.longitude,
40.4168, // Madrid
-3.7038
);
console.log('Distancia a Madrid:', distanceToMadrid.toFixed(2), 'km');
} catch (error) {
console.error('Error obteniendo ubicación:', error.message);
}
}
// Observar cambios de ubicación
// const watchId = GeolocationManager.watchPosition(
// (position) => console.log('Nueva posición:', position),
// (error) => console.error('Error:', error.message)
// );
// Detener observación después de 30 segundos
// setTimeout(() => GeolocationManager.clearWatch(watchId), 30000);
Window.screen - Información de pantalla
// Propiedades de la pantalla
console.log('Ancho total:', window.screen.width);
console.log('Alto total:', window.screen.height);
console.log('Ancho disponible:', window.screen.availWidth);
console.log('Alto disponible:', window.screen.availHeight);
console.log('Profundidad de color:', window.screen.colorDepth);
console.log('Profundidad de píxel:', window.screen.pixelDepth);
console.log('Orientación:', window.screen.orientation?.type);
// Clase para gestionar información de pantalla
class ScreenManager {
// Obtener información completa
static getInfo() {
return {
total: {
width: screen.width,
height: screen.height,
},
available: {
width: screen.availWidth,
height: screen.availHeight,
},
viewport: {
width: window.innerWidth,
height: window.innerHeight,
},
colorDepth: screen.colorDepth,
pixelRatio: window.devicePixelRatio || 1,
orientation: screen.orientation?.type || 'unknown',
touchSupport: 'ontouchstart' in window,
};
}
// Detectar orientación
static getOrientation() {
if (screen.orientation) {
return screen.orientation.type; // 'portrait-primary', 'landscape-primary', etc.
}
// Fallback para navegadores sin soporte
return window.innerHeight > window.innerWidth ? 'portrait' : 'landscape';
}
// Escuchar cambios de orientación
static onOrientationChange(callback) {
if (screen.orientation) {
screen.orientation.addEventListener('change', () => {
callback(screen.orientation.type);
});
} else {
// Fallback usando resize
window.addEventListener('resize', () => {
callback(this.getOrientation());
});
}
}
// Verificar si es pantalla retina
static isRetina() {
return window.devicePixelRatio > 1;
}
// Verificar si está en modo fullscreen
static isFullscreen() {
return !!(
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement
);
}
// Entrar en modo fullscreen
static async enterFullscreen(element = document.documentElement) {
try {
if (element.requestFullscreen) {
await element.requestFullscreen();
} else if (element.webkitRequestFullscreen) {
await element.webkitRequestFullscreen();
} else if (element.mozRequestFullScreen) {
await element.mozRequestFullScreen();
} else if (element.msRequestFullscreen) {
await element.msRequestFullscreen();
}
return true;
} catch (error) {
console.error('Error entrando en fullscreen:', error);
return false;
}
}
// Salir de modo fullscreen
static async exitFullscreen() {
try {
if (document.exitFullscreen) {
await document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
await document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
await document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
await document.msExitFullscreen();
}
return true;
} catch (error) {
console.error('Error saliendo de fullscreen:', error);
return false;
}
}
// Toggle fullscreen
static async toggleFullscreen(element) {
if (this.isFullscreen()) {
return await this.exitFullscreen();
} else {
return await this.enterFullscreen(element);
}
}
}
// Uso del ScreenManager
console.log('Info de pantalla:', ScreenManager.getInfo());
console.log('Orientación:', ScreenManager.getOrientation());
console.log('Es Retina:', ScreenManager.isRetina());
// Escuchar cambios de orientación
ScreenManager.onOrientationChange((orientation) => {
console.log('Nueva orientación:', orientation);
});
// Toggle fullscreen en un elemento
// document.querySelector('#video-player').addEventListener('click', async () => {
// await ScreenManager.toggleFullscreen(document.querySelector('#video-player'));
// });
Timers - Ejecución temporizada
// setTimeout - ejecutar una vez después de un delay
const timeoutId = setTimeout(() => {
console.log('Ejecutado después de 2 segundos');
}, 2000);
// Cancelar timeout
clearTimeout(timeoutId);
// setInterval - ejecutar repetidamente
const intervalId = setInterval(() => {
console.log('Ejecutado cada segundo');
}, 1000);
// Cancelar interval
clearInterval(intervalId);
// Clase para gestionar timers de forma avanzada
class TimerManager {
constructor() {
this.timers = new Map();
this.nextId = 0;
}
// Timeout con identificador propio
timeout(callback, delay, ...args) {
const id = this.nextId++;
const timeoutId = setTimeout(() => {
callback(...args);
this.timers.delete(id);
}, delay);
this.timers.set(id, { type: 'timeout', id: timeoutId });
return id;
}
// Interval con identificador propio
interval(callback, delay, ...args) {
const id = this.nextId++;
const intervalId = setInterval(() => {
callback(...args);
}, delay);
this.timers.set(id, { type: 'interval', id: intervalId });
return id;
}
// Cancelar timer específico
clear(id) {
const timer = this.timers.get(id);
if (timer) {
if (timer.type === 'timeout') {
clearTimeout(timer.id);
} else {
clearInterval(timer.id);
}
this.timers.delete(id);
return true;
}
return false;
}
// Cancelar todos los timers
clearAll() {
for (const [id, timer] of this.timers) {
if (timer.type === 'timeout') {
clearTimeout(timer.id);
} else {
clearInterval(timer.id);
}
}
this.timers.clear();
}
// Debounce - ejecutar solo después de que pasen X ms sin nuevas llamadas
debounce(callback, delay) {
let timeoutId = null;
return (...args) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
callback(...args);
timeoutId = null;
}, delay);
};
}
// Throttle - ejecutar como máximo una vez cada X ms
throttle(callback, delay) {
let lastCall = 0;
let timeoutId = null;
return (...args) => {
const now = Date.now();
const timeSinceLastCall = now - lastCall;
if (timeSinceLastCall >= delay) {
lastCall = now;
callback(...args);
} else {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
lastCall = Date.now();
callback(...args);
}, delay - timeSinceLastCall);
}
};
}
// Retry - reintentar operación con backoff exponencial
async retry(operation, maxAttempts = 3, baseDelay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxAttempts) {
throw error;
}
const delay = baseDelay * Math.pow(2, attempt - 1);
console.log(`Intento ${attempt} falló. Reintentando en ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
// Countdown - cuenta regresiva
countdown(seconds, onTick, onComplete) {
let remaining = seconds;
onTick(remaining);
const intervalId = this.interval(() => {
remaining--;
onTick(remaining);
if (remaining <= 0) {
this.clear(intervalId);
if (onComplete) onComplete();
}
}, 1000);
return intervalId;
}
}
// Uso del TimerManager
const timerManager = new TimerManager();
// Timeout simple
const id1 = timerManager.timeout(() => {
console.log('Timer ejecutado');
}, 2000);
// Interval que se auto-cancela
let count = 0;
const id2 = timerManager.interval(() => {
count++;
console.log('Count:', count);
if (count >= 5) {
timerManager.clear(id2);
}
}, 1000);
// Debounce para búsqueda
const searchInput = document.querySelector('#search');
const debouncedSearch = timerManager.debounce((value) => {
console.log('Buscando:', value);
// Realizar búsqueda
}, 500);
// searchInput?.addEventListener('input', (e) => debouncedSearch(e.target.value));
// Throttle para scroll
const throttledScroll = timerManager.throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 200);
// window.addEventListener('scroll', throttledScroll);
// Retry con backoff exponencial
// async function fetchWithRetry() {
// try {
// const data = await timerManager.retry(
// () => fetch('/api/data').then(r => r.json()),
// 3, // 3 intentos
// 1000 // delay inicial de 1s
// );
// console.log('Datos obtenidos:', data);
// } catch (error) {
// console.error('Error después de reintentos:', error);
// }
// }
// Countdown
timerManager.countdown(
10,
(remaining) => console.log(`Quedan ${remaining} segundos`),
() => console.log('¡Tiempo terminado!')
);
Window sizing y scrolling
// Dimensiones de la ventana
console.log('Ancho interno:', window.innerWidth);
console.log('Alto interno:', window.innerHeight);
console.log('Ancho externo:', window.outerWidth);
console.log('Alto externo:', window.outerHeight);
// Posición de scroll
console.log('Scroll X:', window.scrollX || window.pageXOffset);
console.log('Scroll Y:', window.scrollY || window.pageYOffset);
// Clase para gestionar ventana y scroll
class WindowManager {
// Scroll suave a posición
static scrollTo(x, y, behavior = 'smooth') {
window.scrollTo({
top: y,
left: x,
behavior: behavior,
});
}
// Scroll a elemento específico
static scrollToElement(element, options = {}) {
if (typeof element === 'string') {
element = document.querySelector(element);
}
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest',
...options,
});
}
}
// Obtener posición de scroll
static getScrollPosition() {
return {
x: window.scrollX || window.pageXOffset,
y: window.scrollY || window.pageYOffset,
};
}
// Verificar si está al final del scroll
static isAtBottom(threshold = 100) {
const scrollTop = window.scrollY || window.pageYOffset;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
return scrollTop + windowHeight >= documentHeight - threshold;
}
// Verificar si está al inicio del scroll
static isAtTop(threshold = 0) {
const scrollTop = window.scrollY || window.pageYOffset;
return scrollTop <= threshold;
}
// Obtener porcentaje de scroll
static getScrollPercentage() {
const scrollTop = window.scrollY || window.pageYOffset;
const documentHeight = document.documentElement.scrollHeight;
const windowHeight = window.innerHeight;
return (scrollTop / (documentHeight - windowHeight)) * 100;
}
// Deshabilitar scroll
static disableScroll() {
document.body.style.overflow = 'hidden';
document.documentElement.style.overflow = 'hidden';
}
// Habilitar scroll
static enableScroll() {
document.body.style.overflow = '';
document.documentElement.style.overflow = '';
}
// Smooth scroll con callback
static smoothScrollTo(target, duration = 1000, callback) {
const start = window.scrollY || window.pageYOffset;
const distance = target - start;
const startTime = performance.now();
function animation(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing function (ease in out cubic)
const easeInOutCubic =
progress < 0.5
? 4 * progress * progress * progress
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
window.scrollTo(0, start + distance * easeInOutCubic);
if (progress < 1) {
requestAnimationFrame(animation);
} else if (callback) {
callback();
}
}
requestAnimationFrame(animation);
}
// Redimensionar ventana (solo funciona en ventanas abiertas con window.open)
static resizeWindow(width, height) {
window.resizeTo(width, height);
}
// Mover ventana
static moveWindow(x, y) {
window.moveTo(x, y);
}
// Abrir nueva ventana con configuración
static openWindow(url, name, options = {}) {
const defaultOptions = {
width: 800,
height: 600,
left: screen.width / 2 - 400,
top: screen.height / 2 - 300,
toolbar: 'no',
menubar: 'no',
scrollbars: 'yes',
resizable: 'yes',
};
const finalOptions = { ...defaultOptions, ...options };
const features = Object.entries(finalOptions)
.map(([key, value]) => `${key}=${value}`)
.join(',');
return window.open(url, name, features);
}
}
// Uso del WindowManager
console.log('Posición de scroll:', WindowManager.getScrollPosition());
console.log('Porcentaje scrolleado:', WindowManager.getScrollPercentage().toFixed(2) + '%');
// Scroll suave al top
// WindowManager.scrollTo(0, 0);
// Scroll a elemento
// WindowManager.scrollToElement('#section-2');
// Detectar cuando llega al final
window.addEventListener('scroll', () => {
if (WindowManager.isAtBottom()) {
console.log('¡Llegaste al final!');
// Cargar más contenido
}
});
// Abrir ventana popup centrada
// const popup = WindowManager.openWindow(
// '/login',
// 'LoginWindow',
// { width: 400, height: 500 }
// );
Storage - Almacenamiento local
// LocalStorage - persiste incluso después de cerrar el navegador
localStorage.setItem('usuario', 'Juan');
const usuario = localStorage.getItem('usuario');
localStorage.removeItem('usuario');
localStorage.clear();
// SessionStorage - se borra al cerrar la pestaña
sessionStorage.setItem('token', 'abc123');
const token = sessionStorage.getItem('token');
// Clase para gestionar storage de forma avanzada
class StorageManager {
constructor(storage = localStorage) {
this.storage = storage;
}
// Guardar con serialización automática
set(key, value, expirationMinutes = null) {
const item = {
value: value,
timestamp: Date.now(),
expiration: expirationMinutes ? Date.now() + expirationMinutes * 60 * 1000 : null,
};
try {
this.storage.setItem(key, JSON.stringify(item));
return true;
} catch (error) {
console.error('Error guardando en storage:', error);
return false;
}
}
// Obtener con deserialización automática
get(key, defaultValue = null) {
try {
const itemStr = this.storage.getItem(key);
if (!itemStr) {
return defaultValue;
}
const item = JSON.parse(itemStr);
// Verificar expiración
if (item.expiration && Date.now() > item.expiration) {
this.remove(key);
return defaultValue;
}
return item.value;
} catch (error) {
console.error('Error obteniendo de storage:', error);
return defaultValue;
}
}
// Remover item
remove(key) {
this.storage.removeItem(key);
}
// Limpiar todo
clear() {
this.storage.clear();
}
// Obtener todas las claves
keys() {
return Object.keys(this.storage);
}
// Verificar si existe
has(key) {
return this.storage.getItem(key) !== null;
}
// Obtener tamaño usado
getSize() {
let size = 0;
for (let key in this.storage) {
if (this.storage.hasOwnProperty(key)) {
size += this.storage[key].length + key.length;
}
}
return size; // bytes
}
// Limpiar items expirados
clearExpired() {
const keys = this.keys();
let cleared = 0;
keys.forEach((key) => {
try {
const itemStr = this.storage.getItem(key);
const item = JSON.parse(itemStr);
if (item.expiration && Date.now() > item.expiration) {
this.remove(key);
cleared++;
}
} catch (error) {
// Item inválido, ignorar
}
});
return cleared;
}
// Observar cambios
onChange(callback) {
window.addEventListener('storage', (event) => {
if (event.storageArea === this.storage) {
callback({
key: event.key,
oldValue: event.oldValue,
newValue: event.newValue,
url: event.url,
});
}
});
}
}
// Uso del StorageManager
const storage = new StorageManager(localStorage);
// Guardar datos con expiración de 30 minutos
storage.set('userSession', { id: 123, name: 'Juan' }, 30);
// Obtener datos
const session = storage.get('userSession');
console.log('Sesión:', session);
// Guardar array
storage.set('favoritos', ['item1', 'item2', 'item3']);
// Obtener con valor por defecto
const config = storage.get('appConfig', { theme: 'light', lang: 'es' });
// Verificar tamaño usado
console.log('Storage usado:', (storage.getSize() / 1024).toFixed(2), 'KB');
// Limpiar items expirados
const cleared = storage.clearExpired();
console.log('Items expirados eliminados:', cleared);
// Observar cambios (útil para sincronizar entre pestañas)
storage.onChange((change) => {
console.log('Storage cambió:', change);
});
Performance API
// Medir performance
console.log('Tiempo desde navegación:', performance.now());
console.log('Timing de navegación:', performance.timing);
console.log('Entradas de performance:', performance.getEntries());
// Clase para medir performance
class PerformanceMonitor {
// Marcar punto en el tiempo
static mark(name) {
performance.mark(name);
}
// Medir entre dos marcas
static measure(name, startMark, endMark) {
try {
performance.measure(name, startMark, endMark);
const measure = performance.getEntriesByName(name)[0];
return measure.duration;
} catch (error) {
console.error('Error midiendo performance:', error);
return null;
}
}
// Limpiar marcas y medidas
static clear(name = null) {
if (name) {
performance.clearMarks(name);
performance.clearMeasures(name);
} else {
performance.clearMarks();
performance.clearMeasures();
}
}
// Medir tiempo de ejecución de función
static async measureFunction(fn, name = 'function-execution') {
const startMark = `${name}-start`;
const endMark = `${name}-end`;
this.mark(startMark);
const result = await fn();
this.mark(endMark);
const duration = this.measure(name, startMark, endMark);
console.log(`${name} tomó ${duration?.toFixed(2)}ms`);
return result;
}
// Obtener métricas de navegación
static getNavigationMetrics() {
const timing = performance.timing;
return {
// Tiempo total de carga
totalLoadTime: timing.loadEventEnd - timing.navigationStart,
// Tiempo de respuesta del servidor
serverResponseTime: timing.responseEnd - timing.requestStart,
// Tiempo de renderizado DOM
domRenderTime: timing.domComplete - timing.domLoading,
// Tiempo hasta DOM interactivo
domInteractive: timing.domInteractive - timing.navigationStart,
// Tiempo hasta DOM completo
domComplete: timing.domComplete - timing.navigationStart,
// Tiempo de descarga de página
pageDownloadTime: timing.responseEnd - timing.responseStart,
// Tiempo de redirección
redirectTime: timing.redirectEnd - timing.redirectStart,
// Tiempo de DNS lookup
dnsLookupTime: timing.domainLookupEnd - timing.domainLookupStart,
// Tiempo de conexión TCP
tcpConnectionTime: timing.connectEnd - timing.connectStart,
};
}
// Obtener métricas de recursos
static getResourceMetrics() {
const resources = performance.getEntriesByType('resource');
return resources.map((resource) => ({
name: resource.name,
type: resource.initiatorType,
duration: resource.duration,
size: resource.transferSize,
startTime: resource.startTime,
}));
}
// Observer para nuevas entradas de performance
static observePerformance(callback) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
callback(entry);
}
});
observer.observe({
entryTypes: ['measure', 'navigation', 'resource', 'paint'],
});
return observer;
}
}
// Uso del PerformanceMonitor
console.log('Métricas de navegación:', PerformanceMonitor.getNavigationMetrics());
// Medir operación
PerformanceMonitor.mark('operacion-start');
// ... código a medir ...
PerformanceMonitor.mark('operacion-end');
const duration = PerformanceMonitor.measure('mi-operacion', 'operacion-start', 'operacion-end');
console.log('Operación tomó:', duration, 'ms');
// Medir función
// await PerformanceMonitor.measureFunction(async () => {
// const data = await fetch('/api/data');
// return await data.json();
// }, 'fetch-data');
// Observar performance
const observer = PerformanceMonitor.observePerformance((entry) => {
console.log('Nueva entrada de performance:', entry);
});
Conclusión
El BOM es tu interfaz con el navegador - mientras el DOM te da control sobre el contenido de la página, el BOM te da control sobre el navegador mismo. Juntos forman el arsenal completo para crear aplicaciones web verdaderamente interactivas.
Principios clave para usar el BOM efectivamente:
- Usa las APIs modernas - History API en lugar de manipular location.hash
- Respeta la privacidad - Solicita permisos para geolocalización y notificaciones
- Maneja errores gracefully - No todos los navegadores soportan todas las APIs
- Optimiza el almacenamiento - localStorage tiene límites, usa con moderación
- Mide el performance - Lo que no se mide, no se puede optimizar
El BOM moderno es potente - desde detectar si el usuario está online, hasta medir el performance milisegundo a milisegundo. Pero con gran poder viene gran responsabilidad: usa estas APIs para mejorar la experiencia del usuario, no para rastrearlo o molestarlo.
Recuerda: El BOM varía entre navegadores más que el DOM. Siempre verifica compatibilidad y proporciona fallbacks cuando sea necesario. La detección de características es tu amiga.
El BOM es el puente entre tu código y el mundo exterior - úsalo sabiamente para crear experiencias web que se sientan nativas, responsivas y respetuosas con el usuario.