Skip to content

BOM与定时器

BOM(Browser Object Model,浏览器对象模型)提供了与浏览器交互的对象和方法。本章将介绍 window 对象、location、history、navigator、screen 等核心对象,以及定时器的使用。

window 对象

全局作用域

javascript
// window 是全局对象,所有全局变量和函数都是它的属性
var globalVar = '全局变量';
function globalFunc() {
    console.log('全局函数');
}

console.log(window.globalVar);  // 全局变量
window.globalFunc();  // 全局函数

// let 和 const 声明的变量不会成为 window 的属性
let letVar = 'let 变量';
console.log(window.letVar);  // undefined

// 内置对象也是 window 的属性
console.log(window.document);
console.log(window.console);
console.log(window.Math);
console.log(window.Date);

窗口尺寸

javascript
// 获取窗口尺寸
console.log(window.innerWidth);   // 视口宽度(包含滚动条)
console.log(window.innerHeight);  // 视口高度

// 获取文档尺寸
console.log(document.documentElement.clientWidth);   // 视口宽度(不含滚动条)
console.log(document.documentElement.clientHeight);  // 视口高度

// 获取整个窗口尺寸(包含工具栏等)
console.log(window.outerWidth);
console.log(window.outerHeight);

// 滚动位置
console.log(window.scrollX);  // 水平滚动距离
console.log(window.scrollY);  // 垂直滚动距离
// 或
console.log(window.pageXOffset);
console.log(window.pageYOffset);

// 滚动方法
window.scrollTo(0, 0);  // 滚动到指定位置
window.scrollTo({
    top: 0,
    left: 0,
    behavior: 'smooth'  // 平滑滚动
});

window.scrollBy(0, 100);  // 相对滚动
window.scrollBy({
    top: 100,
    behavior: 'smooth'
});

窗口操作

javascript
// 打开新窗口
let newWindow = window.open('https://example.com', '_blank', 'width=800,height=600');

// 参数说明:
// 第一个参数:URL
// 第二个参数:目标(_blank, _self, _parent, _top 或窗口名称)
// 第三个参数:窗口特性字符串

// 窗口特性
window.open('https://example.com', '_blank', 
    'width=800,height=600,left=100,top=100,menubar=no,toolbar=no');

// 关闭窗口
// newWindow.close();

// 检查窗口是否已关闭
console.log(newWindow.closed);  // false

// 移动窗口(需要用户允许)
// window.moveTo(100, 100);
// window.moveBy(50, 50);

// 调整窗口大小(需要用户允许)
// window.resizeTo(800, 600);
// window.resizeBy(100, 100);

// 打印
window.print();

// 弹窗方法
alert('警告框');
let confirmed = confirm('确认框');
let input = prompt('输入框', '默认值');

location 对象

URL 信息

javascript
// location 对象包含当前 URL 的信息
console.log(location.href);     // 完整 URL
console.log(location.protocol); // 协议(http: 或 https:)
console.log(location.host);     // 主机名和端口
console.log(location.hostname); // 主机名
console.log(location.port);     // 端口
console.log(location.pathname); // 路径
console.log(location.search);   // 查询字符串(?后面的部分)
console.log(location.hash);     // 片段标识符(#后面的部分)
console.log(location.origin);   // 源(协议+主机+端口)

// 示例
// URL: https://example.com:8080/path/page.html?id=123#section
// href:     https://example.com:8080/path/page.html?id=123#section
// protocol: https:
// host:     example.com:8080
// hostname: example.com
// port:     8080
// pathname: /path/page.html
// search:   ?id=123
// hash:     #section
// origin:   https://example.com:8080

页面跳转

javascript
// 跳转到新页面(会记录历史)
location.href = 'https://example.com';
// 或
location.assign('https://example.com');

// 替换当前页面(不记录历史,无法后退)
location.replace('https://example.com');

// 重新加载页面
location.reload();        // 从缓存重新加载
location.reload(true);    // 从服务器重新加载

// 修改 URL 的各个部分
location.hash = '#new-section';      // 修改片段
location.search = '?page=2';         // 修改查询字符串
location.pathname = '/new-path';     // 修改路径

解析查询字符串

javascript
// 获取查询参数
let searchParams = new URLSearchParams(location.search);

// 获取单个参数
let id = searchParams.get('id');
let name = searchParams.get('name');

// 检查参数是否存在
console.log(searchParams.has('id'));

// 获取所有同名参数
let tags = searchParams.getAll('tag');

// 遍历所有参数
for (let [key, value] of searchParams) {
    console.log(key, value);
}

// 添加/修改参数
searchParams.set('page', '2');
searchParams.append('tag', 'new');

// 删除参数
searchParams.delete('id');

// 转换为字符串
let newSearch = searchParams.toString();

// 解析完整 URL
let url = new URL('https://example.com/path?name=test&id=123#section');
console.log(url.hostname);  // example.com
console.log(url.pathname);  // /path
console.log(url.searchParams.get('name'));  // test

history 对象

历史记录导航

javascript
// history 对象管理浏览历史

// 历史记录数量
console.log(history.length);

// 前进/后退
history.back();      // 后退一步
history.forward();   // 前进一步
history.go(-1);      // 后退一步
history.go(1);       // 前进一步
history.go(0);       // 刷新当前页面
history.go(-2);      // 后退两步

History API

javascript
// pushState:添加历史记录
history.pushState({ page: 1 }, '标题', '/page1');

// 参数:
// state: 状态对象
// title: 标题(目前大多数浏览器忽略)
// url: 新的 URL

// replaceState:替换当前历史记录
history.replaceState({ page: 2 }, '', '/page2');

// 监听历史记录变化
window.addEventListener('popstate', function(e) {
    console.log('历史记录改变');
    console.log('状态:', e.state);
});

// 示例:单页应用路由
function navigateTo(path) {
    history.pushState({ path }, '', path);
    renderPage(path);
}

window.addEventListener('popstate', function(e) {
    if (e.state) {
        renderPage(e.state.path);
    }
});

function renderPage(path) {
    console.log('渲染页面:', path);
}

浏览器信息

javascript
// navigator 对象包含浏览器信息

// 用户代理字符串
console.log(navigator.userAgent);

// 浏览器名称和版本(已废弃,不推荐使用)
console.log(navigator.appName);
console.log(navigator.appVersion);

// 平台
console.log(navigator.platform);

// 语言
console.log(navigator.language);
console.log(navigator.languages);

// 是否在线
console.log(navigator.onLine);

// Cookie 是否启用
console.log(navigator.cookieEnabled);

// 产品名称
console.log(navigator.product);

// 检测浏览器类型
function getBrowser() {
    let ua = navigator.userAgent;
    if (ua.includes('Chrome')) return 'Chrome';
    if (ua.includes('Firefox')) return 'Firefox';
    if (ua.includes('Safari')) return 'Safari';
    if (ua.includes('Edge')) return 'Edge';
    if (ua.includes('IE')) return 'IE';
    return 'Unknown';
}

// 检测移动设备
function isMobile() {
    return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}

地理位置

javascript
// 获取地理位置
navigator.geolocation.getCurrentPosition(
    // 成功回调
    function(position) {
        console.log('纬度:', position.coords.latitude);
        console.log('经度:', position.coords.longitude);
        console.log('精度:', position.coords.accuracy);
        console.log('海拔:', position.coords.altitude);
        console.log('速度:', position.coords.speed);
    },
    // 错误回调
    function(error) {
        console.log('错误:', error.message);
    },
    // 选项
    {
        enableHighAccuracy: true,  // 高精度
        timeout: 5000,             // 超时时间
        maximumAge: 0              // 缓存时间
    }
);

// 持续监听位置变化
let watchId = navigator.geolocation.watchPosition(
    function(position) {
        console.log('位置更新:', position.coords);
    }
);

// 停止监听
navigator.geolocation.clearWatch(watchId);

剪贴板 API

javascript
// 写入剪贴板
navigator.clipboard.writeText('要复制的文本')
    .then(() => console.log('复制成功'))
    .catch(err => console.log('复制失败:', err));

// 读取剪贴板
navigator.clipboard.readText()
    .then(text => console.log('剪贴板内容:', text))
    .catch(err => console.log('读取失败:', err));

// 复制按钮示例
function copyToClipboard(text) {
    navigator.clipboard.writeText(text)
        .then(() => alert('复制成功'))
        .catch(() => {
            // 降级方案
            let textarea = document.createElement('textarea');
            textarea.value = text;
            document.body.appendChild(textarea);
            textarea.select();
            document.execCommand('copy');
            document.body.removeChild(textarea);
            alert('复制成功');
        });
}

其他 API

javascript
// 电池状态 API
navigator.getBattery().then(function(battery) {
    console.log('电量:', battery.level * 100 + '%');
    console.log('是否充电:', battery.charging);
    
    battery.addEventListener('levelchange', function() {
        console.log('电量变化:', battery.level);
    });
});

// 网络连接 API
console.log(navigator.connection.effectiveType);  // 4g, 3g, 2g
console.log(navigator.connection.downlink);       // 下行速度

navigator.connection.addEventListener('change', function() {
    console.log('网络状态变化');
});

// 分享 API
if (navigator.share) {
    navigator.share({
        title: '分享标题',
        text: '分享内容',
        url: 'https://example.com'
    })
    .then(() => console.log('分享成功'))
    .catch(err => console.log('分享失败:', err));
}

// 在线/离线事件
window.addEventListener('online', function() {
    console.log('网络已连接');
});

window.addEventListener('offline', function() {
    console.log('网络已断开');
});

screen 对象

javascript
// screen 对象包含屏幕信息

// 屏幕尺寸
console.log(screen.width);    // 屏幕宽度
console.log(screen.height);   // 屏幕高度

// 可用屏幕尺寸(排除任务栏等)
console.log(screen.availWidth);
console.log(screen.availHeight);

// 颜色深度
console.log(screen.colorDepth);  // 颜色深度(位)
console.log(screen.pixelDepth);  // 像素深度

// 设备像素比(高清屏)
console.log(window.devicePixelRatio);  // 1 = 普通屏, 2 = Retina 屏

// 检测屏幕方向
console.log(screen.orientation.type);  // portrait-primary, landscape-primary

screen.orientation.addEventListener('change', function() {
    console.log('屏幕方向改变:', screen.orientation.type);
});

定时器

setTimeout

javascript
// setTimeout:延迟执行一次
let timerId = setTimeout(function() {
    console.log('1秒后执行');
}, 1000);

// 传递参数
setTimeout(function(name, age) {
    console.log(name, age);
}, 1000, '张三', 25);

// 使用箭头函数
setTimeout(() => {
    console.log('箭头函数');
}, 1000);

// 清除定时器
clearTimeout(timerId);

// 立即执行(延迟 0 毫秒)
setTimeout(() => {
    console.log('下一个事件循环执行');
}, 0);

console.log('同步代码');
// 输出顺序:同步代码 -> 下一个事件循环执行

setInterval

javascript
// setInterval:间隔重复执行
let count = 0;
let intervalId = setInterval(function() {
    count++;
    console.log('执行次数:', count);
    
    if (count >= 5) {
        clearInterval(intervalId);  // 停止
    }
}, 1000);

// 清除定时器
clearInterval(intervalId);

// 注意:setInterval 可能会累积
// 如果处理函数执行时间超过间隔时间,会导致多个处理函数排队

// 更安全的间隔执行方式
function safeInterval(callback, delay) {
    let running = false;
    
    setInterval(() => {
        if (running) return;
        running = true;
        callback();
        running = false;
    }, delay);
}

递归 setTimeout

javascript
// 使用递归 setTimeout 替代 setInterval
// 可以确保上一次执行完成后再开始下一次

function repeatWithDelay() {
    console.log('执行');
    
    setTimeout(repeatWithDelay, 1000);
}

repeatWithDelay();

// 带停止条件的递归
function repeatWithCondition(max, current = 0) {
    console.log('执行:', current);
    
    if (current < max) {
        setTimeout(() => {
            repeatWithCondition(max, current + 1);
        }, 1000);
    }
}

repeatWithCondition(5);

// 可取消的递归定时器
class RepeatingTimer {
    constructor(callback, delay) {
        this.callback = callback;
        this.delay = delay;
        this.timerId = null;
        this.running = false;
    }
    
    start() {
        this.running = true;
        this.tick();
    }
    
    tick() {
        if (!this.running) return;
        
        this.timerId = setTimeout(() => {
            this.callback();
            this.tick();
        }, this.delay);
    }
    
    stop() {
        this.running = false;
        if (this.timerId) {
            clearTimeout(this.timerId);
        }
    }
}

let timer = new RepeatingTimer(() => console.log('tick'), 1000);
timer.start();
// timer.stop();

requestAnimationFrame

javascript
// requestAnimationFrame:用于动画,与屏幕刷新同步
// 通常每秒 60 次(约 16.67ms)

function animate() {
    // 更新动画
    console.log('动画帧');
    
    requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

// 停止动画
let animationId;
function startAnimation() {
    function frame() {
        // 更新动画
        animationId = requestAnimationFrame(frame);
    }
    animationId = requestAnimationFrame(frame);
}

function stopAnimation() {
    cancelAnimationFrame(animationId);
}

// 动画示例:移动元素
let element = document.getElementById('box');
let position = 0;

function moveRight() {
    position += 2;
    element.style.transform = `translateX(${position}px)`;
    
    if (position < 300) {
        requestAnimationFrame(moveRight);
    }
}

requestAnimationFrame(moveRight);

// 计算帧间隔时间
let lastTime = 0;
function animateWithDelta(timestamp) {
    let deltaTime = timestamp - lastTime;
    lastTime = timestamp;
    
    console.log('帧间隔:', deltaTime, 'ms');
    
    requestAnimationFrame(animateWithDelta);
}

requestAnimationFrame(animateWithDelta);

定时器管理

javascript
// 封装定时器管理
class TimerManager {
    constructor() {
        this.timers = new Map();
        this.intervals = new Map();
        this.animations = new Map();
    }
    
    // 添加延迟定时器
    setTimeout(name, callback, delay) {
        this.clearTimeout(name);
        const id = setTimeout(callback, delay);
        this.timers.set(name, id);
        return id;
    }
    
    // 清除延迟定时器
    clearTimeout(name) {
        if (this.timers.has(name)) {
            clearTimeout(this.timers.get(name));
            this.timers.delete(name);
        }
    }
    
    // 添加间隔定时器
    setInterval(name, callback, delay) {
        this.clearInterval(name);
        const id = setInterval(callback, delay);
        this.intervals.set(name, id);
        return id;
    }
    
    // 清除间隔定时器
    clearInterval(name) {
        if (this.intervals.has(name)) {
            clearInterval(this.intervals.get(name));
            this.intervals.delete(name);
        }
    }
    
    // 添加动画帧
    requestAnimationFrame(name, callback) {
        this.cancelAnimationFrame(name);
        const animate = () => {
            callback();
            const id = requestAnimationFrame(animate);
            this.animations.set(name, id);
        };
        const id = requestAnimationFrame(animate);
        this.animations.set(name, id);
        return id;
    }
    
    // 取消动画帧
    cancelAnimationFrame(name) {
        if (this.animations.has(name)) {
            cancelAnimationFrame(this.animations.get(name));
            this.animations.delete(name);
        }
    }
    
    // 清除所有定时器
    clearAll() {
        this.timers.forEach(id => clearTimeout(id));
        this.intervals.forEach(id => clearInterval(id));
        this.animations.forEach(id => cancelAnimationFrame(id));
        this.timers.clear();
        this.intervals.clear();
        this.animations.clear();
    }
}

// 使用
const timerManager = new TimerManager();

timerManager.setInterval('tick', () => {
    console.log('tick');
}, 1000);

timerManager.setTimeout('delay', () => {
    console.log('延迟执行');
}, 3000);

// 清除特定定时器
timerManager.clearInterval('tick');

// 清除所有
timerManager.clearAll();

实战案例

倒计时

javascript
function countdown(seconds, callback) {
    let remaining = seconds;
    
    let timer = setInterval(() => {
        callback(remaining);
        
        if (remaining <= 0) {
            clearInterval(timer);
            return;
        }
        
        remaining--;
    }, 1000);
    
    return timer;
}

// 使用
countdown(10, (remaining) => {
    if (remaining > 0) {
        console.log(`剩余 ${remaining} 秒`);
    } else {
        console.log('倒计时结束!');
    }
});

防抖函数

javascript
function debounce(func, delay) {
    let timer = null;
    
    return function(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// 使用
let handleResize = debounce(() => {
    console.log('窗口大小改变');
}, 300);

window.addEventListener('resize', handleResize);

节流函数

javascript
function throttle(func, delay) {
    let lastTime = 0;
    
    return function(...args) {
        let now = Date.now();
        
        if (now - lastTime >= delay) {
            lastTime = now;
            func.apply(this, args);
        }
    };
}

// 使用
let handleScroll = throttle(() => {
    console.log('滚动位置:', window.scrollY);
}, 200);

window.addEventListener('scroll', handleScroll);

小结

本章学习了 BOM 和定时器的核心知识:

  • window 对象:全局作用域、窗口尺寸、窗口操作
  • location 对象:URL 信息、页面跳转、查询字符串
  • history 对象:历史记录导航、History API
  • navigator 对象:浏览器信息、地理位置、剪贴板
  • screen 对象:屏幕信息
  • 定时器:setTimeout、setInterval、requestAnimationFrame

下一章我们将学习 ES6+新特性,了解现代 JavaScript 的新功能。