03-DOM与BOM操作

📋 学习目标

  • 掌握DOM操作核心API

  • 学习事件处理机制

  • 理解BOM浏览器对象

  • 掌握性能优化技巧

🌳 DOM操作

元素选择

// getElementById
const element = document.getElementById('myId');

// querySelector(推荐)
const element = document.querySelector('.class');
const element = document.querySelector('#id');
const element = document.querySelector('[data-id="123"]');

// querySelectorAll
const elements = document.querySelectorAll('.items');
// 返回NodeList,可以forEach
elements.forEach(el => console.log(el));

// 转换为数组
const array = Array.from(elements);
const array = [...elements];

// 其他选择器(不推荐)
document.getElementsByClassName('class');
document.getElementsByTagName('div');
document.getElementsByName('name');

元素创建与插入

// 创建元素
const div = document.createElement('div');
div.className = 'container';
div.id = 'main';
div.textContent = 'Hello';

// 设置HTML
div.innerHTML = '<span>Content</span>';

// 设置属性
div.setAttribute('data-id', '123');
div.dataset.id = '123';  // 推荐

// 插入元素
document.body.appendChild(div);
document.body.append(div, 'text', anotherElement);

// 在前面插入
parent.insertBefore(newNode, referenceNode);
parent.prepend(newNode);

// 在后面插入
parent.after(newNode);

// 替换
parent.replaceChild(newNode, oldNode);

// 移除
element.remove();
parent.removeChild(child);

// 克隆
const clone = element.cloneNode(true);  // true表示深拷贝

元素属性操作

// 类名操作
element.classList.add('active');
element.classList.remove('active');
element.classList.toggle('active');
element.classList.contains('active');
element.classList.replace('old', 'new');

// 样式操作
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.cssText = 'color: red; font-size: 16px;';

// 获取计算样式
const styles = window.getComputedStyle(element);
const color = styles.getPropertyValue('color');

// 属性操作
element.getAttribute('data-id');
element.setAttribute('data-id', '123');
element.removeAttribute('data-id');
element.hasAttribute('data-id');

// dataset(推荐)
element.dataset.userId = '123';  // <div data-user-id="123">
const userId = element.dataset.userId;

DOM遍历

// 父元素
element.parentElement;
element.parentNode;

// 子元素
element.children;  // HTMLCollection
element.childNodes;  // NodeList(包含文本节点)
element.firstElementChild;
element.lastElementChild;

// 兄弟元素
element.nextElementSibling;
element.previousElementSibling;

// 最近的匹配元素
element.closest('.container');

// 检查包含关系
parent.contains(child);

// 遍历所有子元素
for (const child of element.children) {
    console.log(child);
}

📅 事件处理

事件监听

// 添加事件监听
element.addEventListener('click', function(e) {
    console.log('Clicked!', e);
});

// 箭头函数(注意this指向)
element.addEventListener('click', (e) => {
    console.log(e.target);
});

// 移除事件监听
function handleClick(e) {
    console.log('Clicked');
}
element.addEventListener('click', handleClick);
element.removeEventListener('click', handleClick);

// 只执行一次
element.addEventListener('click', handleClick, {once: true});

// 捕获阶段
element.addEventListener('click', handleClick, {capture: true});

// 阻止默认行为
element.addEventListener('click', (e) => {
    e.preventDefault();
});

// 阻止冒泡
element.addEventListener('click', (e) => {
    e.stopPropagation();
});

事件委托

// ❌ 为每个元素添加监听
document.querySelectorAll('.item').forEach(item => {
    item.addEventListener('click', handleClick);
});

// ✅ 事件委托(推荐)
document.querySelector('.list').addEventListener('click', (e) => {
    if (e.target.matches('.item')) {
        handleItemClick(e.target);
    }
});

// 实际应用
const list = document.querySelector('.todo-list');

list.addEventListener('click', (e) => {
    const target = e.target;
    
    if (target.matches('.delete-btn')) {
        const item = target.closest('.todo-item');
        item.remove();
    }
    
    if (target.matches('.toggle-btn')) {
        const item = target.closest('.todo-item');
        item.classList.toggle('completed');
    }
});

常用事件

// 鼠标事件
element.addEventListener('click', handler);
element.addEventListener('dblclick', handler);
element.addEventListener('mousedown', handler);
element.addEventListener('mouseup', handler);
element.addEventListener('mousemove', handler);
element.addEventListener('mouseenter', handler);
element.addEventListener('mouseleave', handler);
element.addEventListener('mouseover', handler);
element.addEventListener('mouseout', handler);

// 键盘事件
element.addEventListener('keydown', (e) => {
    console.log(e.key, e.code, e.keyCode);
    if (e.key === 'Enter') {
        // 处理回车
    }
    if (e.ctrlKey && e.key === 's') {
        e.preventDefault();
        // Ctrl+S保存
    }
});

element.addEventListener('keyup', handler);
element.addEventListener('keypress', handler);  // 已废弃

// 表单事件
input.addEventListener('input', (e) => {
    console.log(e.target.value);
});
input.addEventListener('change', handler);
input.addEventListener('focus', handler);
input.addEventListener('blur', handler);
form.addEventListener('submit', (e) => {
    e.preventDefault();
    // 处理提交
});

// 窗口事件
window.addEventListener('load', handler);  // 页面完全加载
window.addEventListener('DOMContentLoaded', handler);  // DOM加载完成
window.addEventListener('beforeunload', (e) => {
    e.returnValue = '';  // 离开页面提示
});
window.addEventListener('resize', handler);
window.addEventListener('scroll', handler);

自定义事件

// 创建自定义事件
const event = new CustomEvent('myEvent', {
    detail: {
        message: 'Hello',
        value: 123
    },
    bubbles: true,
    cancelable: true
});

// 监听自定义事件
element.addEventListener('myEvent', (e) => {
    console.log(e.detail.message);
});

// 触发事件
element.dispatchEvent(event);

// 实际应用:组件通信
class Component {
    constructor(element) {
        this.element = element;
    }
    
    emit(eventName, data) {
        const event = new CustomEvent(eventName, {
            detail: data,
            bubbles: true
        });
        this.element.dispatchEvent(event);
    }
    
    on(eventName, handler) {
        this.element.addEventListener(eventName, handler);
    }
}

const comp = new Component(document.querySelector('.component'));
comp.on('update', (e) => {
    console.log('Updated:', e.detail);
});
comp.emit('update', {value: 'new value'});

🌐 BOM操作

window对象

// 窗口尺寸
window.innerWidth;   // 视口宽度
window.innerHeight;  // 视口高度
window.outerWidth;   // 浏览器窗口宽度
window.outerHeight;  // 浏览器窗口高度

// 滚动
window.scrollX;  // 水平滚动距离
window.scrollY;  // 垂直滚动距离

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

window.scrollBy(0, 100);  // 相对滚动

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

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

// 对话框
window.alert('提示信息');
const result = window.confirm('确认吗?');
const input = window.prompt('请输入:', '默认值');

location对象

// URL信息
location.href;      // 完整URL
location.protocol;  // 协议(http:)
location.host;      // 主机名和端口
location.hostname;  // 主机名
location.port;      // 端口
location.pathname;  // 路径
location.search;    // 查询字符串(?key=value)
location.hash;      // 锚点(#section)

// 导航
location.href = 'https://example.com';  // 跳转
location.assign('https://example.com'); // 跳转
location.replace('https://example.com'); // 替换(不产生历史记录)
location.reload();  // 刷新
location.reload(true);  // 强制从服务器刷新

// URLSearchParams
const params = new URLSearchParams(location.search);
params.get('id');  // 获取参数
params.set('page', '2');  // 设置参数
params.delete('old');  // 删除参数
params.toString();  // 转为字符串

// 实际应用
function getQueryParams() {
    const params = new URLSearchParams(location.search);
    const result = {};
    for (const [key, value] of params) {
        result[key] = value;
    }
    return result;
}

history对象

// 导航
history.back();     // 后退
history.forward();  // 前进
history.go(-2);     // 后退2页
history.go(1);      // 前进1页

// HTML5 History API
history.pushState({page: 1}, 'title', '/page1');
history.replaceState({page: 2}, 'title', '/page2');

// 监听历史变化
window.addEventListener('popstate', (e) => {
    console.log('State:', e.state);
});

// 实际应用:单页应用路由
class Router {
    constructor() {
        this.routes = {};
        window.addEventListener('popstate', () => {
            this.handleRoute();
        });
    }
    
    route(path, handler) {
        this.routes[path] = handler;
    }
    
    navigate(path) {
        history.pushState({}, '', path);
        this.handleRoute();
    }
    
    handleRoute() {
        const path = location.pathname;
        const handler = this.routes[path];
        if (handler) handler();
    }
}

screen对象

screen.width;   // 屏幕宽度
screen.height;  // 屏幕高度
screen.availWidth;   // 可用宽度
screen.availHeight;  // 可用高度
screen.colorDepth;   // 颜色深度
screen.pixelDepth;   // 像素深度

⚡ 性能优化

防抖和节流

// 防抖
function debounce(fn, delay) {
    let timer = null;
    return function(...args) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}

// 使用
const handleInput = debounce((e) => {
    console.log('Search:', e.target.value);
}, 500);

input.addEventListener('input', handleInput);

// 节流
function throttle(fn, delay) {
    let lastCall = 0;
    return function(...args) {
        const now = Date.now();
        if (now - lastCall >= delay) {
            lastCall = now;
            fn.apply(this, args);
        }
    };
}

// 使用
const handleScroll = throttle(() => {
    console.log('Scrolled');
}, 200);

window.addEventListener('scroll', handleScroll);

DocumentFragment

// ❌ 多次操作DOM
for (let i = 0; i < 1000; i++) {
    const li = document.createElement('li');
    li.textContent = `Item ${i}`;
    ul.appendChild(li);  // 触发1000次重排
}

// ✅ 使用DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    const li = document.createElement('li');
    li.textContent = `Item ${i}`;
    fragment.appendChild(li);
}
ul.appendChild(fragment);  // 只触发1次重排

IntersectionObserver

// 懒加载图片
const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            observer.unobserve(img);
        }
    });
});

document.querySelectorAll('img[data-src]').forEach(img => {
    observer.observe(img);
});

// 无限滚动
const sentinel = document.querySelector('.sentinel');
const observer = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting) {
        loadMore();
    }
});
observer.observe(sentinel);

📚 实践练习

练习1:Todo列表

实现功能:

  • 添加、删除Todo

  • 标记完成

  • 过滤显示

  • 本地存储

练习2:图片懒加载

实现:

  • IntersectionObserver

  • 占位图

  • 加载动画

练习3:无限滚动

实现:

  • 滚动加载

  • 加载状态

  • 错误处理

📚 参考资料