主题
第 07 章 · DOM 与 BOM(操作页面与浏览器)
一句话开篇:本章告诉你"JS 怎么操作页面(DOM)和浏览器(BOM)"——节点增删改查、事件机制、事件委托、地址栏跳转,是写出活页面的基本功。
0. 生活类比(先建立直觉)
把网页想象成一栋"活的房子":
| 概念 | 类比 |
|---|---|
| DOM | 房子的骨架与房间布局(家具、墙壁可以增删改) |
| 节点 | 每个房间、每件家具 |
| 事件 | 房间里发生的"事"——按门铃、灯亮了、有人喊话 |
| 事件冒泡 | 你在卧室喊一声,客厅、家门外都能听到(一层层往外传) |
| 事件捕获 | 物业从大门外开始一个个房间确认有没有声音 |
| 事件委托 | 不在每个房间都装监听器,只在客厅装一个统一收(省力高效) |
| BOM | 房子的"水电气燃管理系统"(地址栏 location、历史 history、屏幕 screen) |
| window | 整栋房子(最外层全局对象) |
1. DOM 是什么
DOM(Document Object Model,文档对象模型):HTML 加载后,浏览器把它解析成一棵"对象树",JS 可以通过这棵树读写页面。
text
document
└── html
├── head
│ ├── title
│ └── meta
└── body
├── div#app
│ ├── h1
│ └── ul
│ ├── li
│ └── li
└── script每个标签 = 一个元素节点;文本 = 文本节点;属性 = 属性节点。
节点类型常用速查
| nodeType | 含义 |
|---|---|
| 1 | 元素节点(Element) |
| 3 | 文本节点(Text) |
| 8 | 注释节点 |
| 9 | document 节点 |
2. DOM 查询
2.1 经典 API
js
// 单个
document.getElementById('app'); // 通过 id
document.querySelector('.box > p'); // CSS 选择器,返回第一个
// 多个(NodeList,类数组)
document.getElementsByTagName('li'); // HTMLCollection(实时!)
document.getElementsByClassName('item'); // HTMLCollection(实时!)
document.querySelectorAll('.item'); // NodeList(静态)2.2 NodeList vs HTMLCollection
| 维度 | NodeList | HTMLCollection |
|---|---|---|
| 来源 | querySelectorAll、childNodes | getElementsBy* |
| 实时性 | 静态快照(querySelectorAll) / 实时(childNodes) | 实时(DOM 变化它也变) |
| 包含 | 元素 + 文本 + 注释(childNodes) | 仅元素 |
| 可用 forEach | ✅ | ❌(要 Array.from() 转) |
js
const lis = document.getElementsByTagName('li');
console.log(lis.length); // 假设 3
ul.appendChild(document.createElement('li'));
console.log(lis.length); // 4 ← 实时变了!2.3 节点关系
js
const el = document.querySelector('.item');
el.parentNode; // 父节点
el.children; // 子元素(HTMLCollection)
el.childNodes; // 子节点(含文本、注释)
el.firstElementChild; // 第一个子元素
el.lastElementChild;
el.nextElementSibling; // 下一个兄弟元素
el.previousElementSibling;3. DOM 增删改
3.1 创建节点
js
const div = document.createElement('div');
div.textContent = 'hello'; // 设置文本
div.innerHTML = '<b>HTML</b>'; // 设置 HTML(注意 XSS)
div.className = 'box'; // 设置 class
div.id = 'mydiv';
div.setAttribute('data-id', '123');3.2 插入节点
js
parent.appendChild(child); // 追加到末尾
parent.insertBefore(newNode, refNode); // 插入到 refNode 之前
// ES2017:更直观的 API
parent.append(node1, '文本', node2); // 末尾
parent.prepend(node); // 开头
existing.before(node); // 之前
existing.after(node); // 之后
existing.replaceWith(node); // 替换3.3 删除节点
js
parent.removeChild(child);
node.remove(); // 更现代3.4 修改样式
js
el.style.color = 'red'; // 内联样式(覆盖原值)
el.style.cssText = 'color:red; font-size:14px';
el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('open');
el.classList.contains('active');💡 批量改样式优先用 className 切换,比逐条改 style 快且好维护。
4. 事件机制 ⭐
4.1 事件流的三个阶段
text
┌── 捕获阶段 ──┐
│ (从外向内) │
▼ │
window → document → html → body → div → button
│
▼
目标阶段
│
▼
window ← document ← html ← body ← div ← button
▲ │
│ (从内向外) │
└── 冒泡阶段 ────────┘- 捕获阶段:从 window 向目标传递
- 目标阶段:到达事件目标
- 冒泡阶段:从目标向 window 传递
4.2 addEventListener
js
target.addEventListener('click', handler, options);
// options 可以是布尔值(是否捕获)或对象
target.addEventListener('click', handler, {
capture: false, // true 捕获阶段触发,false 冒泡阶段(默认)
once: true, // 只触发一次
passive: true, // 承诺不调用 preventDefault(提升滚动性能)
signal: ctrl.signal // AbortController 控制取消
});4.3 事件对象常用属性
js
btn.addEventListener('click', (e) => {
e.target; // 真正被点的元素
e.currentTarget; // 监听器所在元素(== this)
e.type; // 'click'
e.preventDefault(); // 阻止默认行为(如 a 标签跳转)
e.stopPropagation();// 阻止冒泡
e.stopImmediatePropagation(); // 阻止冒泡且阻止同元素其他监听器
});4.4 e.target vs e.currentTarget
js
// HTML: <ul><li>A</li><li>B</li></ul>
ul.addEventListener('click', (e) => {
console.log(e.target); // 被点击的 <li>
console.log(e.currentTarget); // <ul> 本身
});💡 这是事件委托的基础!
5. 事件委托(Event Delegation)⭐
5.1 痛点
要给 100 个 <li> 都加点击事件?太浪费。还涉及到动态添加的元素(新加的 li 没有监听器)。
5.2 解决:把事件挂在父节点上
html
<ul id="list">
<li>苹果</li>
<li>香蕉</li>
<li>橘子</li>
</ul>js
document.getElementById('list').addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
console.log('点击了:', e.target.textContent);
}
});优点:
- 只挂一个监听器,性能好
- 动态新增的子元素自动生效
- 减少内存占用
坑:
- 事件必须能冒泡(少数事件如
focus/blur不冒泡,要用focusin/focusout) - 注意精准识别目标(用
closest()处理嵌套)
js
// 用 closest 健壮匹配
ul.addEventListener('click', (e) => {
const li = e.target.closest('li');
if (!li || !ul.contains(li)) return;
// 处理 li
});6. 自定义事件
js
// 创建并派发
const ev = new CustomEvent('my-event', {
detail: { id: 1, name: 'Tom' },
bubbles: true
});
el.dispatchEvent(ev);
// 监听
el.addEventListener('my-event', (e) => console.log(e.detail));7. BOM(浏览器对象模型)
BOM 是浏览器提供的操作浏览器本身的 API(不是页面内容)。核心对象都挂在 window 上。
7.1 window:全局对象
js
window.innerWidth; // 视口宽
window.innerHeight; // 视口高
window.scrollX, window.scrollY;
window.scrollTo(0, 100);
window.setTimeout / setInterval
window.alert / confirm / prompt7.2 location:URL 操作
js
location.href; // 完整 URL
location.protocol; // 'https:'
location.host; // 'example.com:8080'
location.hostname; // 'example.com'
location.port; // '8080'
location.pathname; // '/path/to/page'
location.search; // '?a=1&b=2'
location.hash; // '#section'
location.href = 'https://google.com'; // 跳转
location.assign('https://google.com'); // 跳转
location.replace('...'); // 跳转,不留历史
location.reload(); // 刷新7.3 history:历史记录
js
history.length; // 历史条数
history.back(); // 后退
history.forward(); // 前进
history.go(-1); // 等价 back()
// 修改 URL 但不刷新(SPA 路由核心)
history.pushState({ page: 1 }, 'title', '/new-url');
history.replaceState(state, '', '/another');
// 监听浏览器前进后退
window.addEventListener('popstate', (e) => {
console.log('state:', e.state);
});7.4 navigator:浏览器信息
js
navigator.userAgent;
navigator.language;
navigator.onLine; // 是否在线
navigator.clipboard.writeText('复制内容');
navigator.geolocation.getCurrentPosition(pos => {});7.5 screen:屏幕信息
js
screen.width; // 屏幕宽
screen.height;
screen.availWidth; // 可用宽(去掉任务栏)
window.devicePixelRatio; // 物理像素比(高清屏 >1)8. 常用 API 速查
| 需求 | API |
|---|---|
| 等 DOM 加载完 | DOMContentLoaded |
| 等所有资源 | load |
| 获取元素位置 | el.getBoundingClientRect() |
| 滚动到元素 | el.scrollIntoView({ behavior: 'smooth' }) |
| 复制到剪贴板 | navigator.clipboard.writeText(text) |
| 监听元素尺寸变化 | ResizeObserver |
| 监听元素出现 | IntersectionObserver(懒加载、无限滚动) |
| 监听 DOM 变化 | MutationObserver |
IntersectionObserver 示例(懒加载图片)
js
const io = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.src = e.target.dataset.src;
io.unobserve(e.target);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));9. ⚠️ 易踩的坑
- 坑 1:getElementsByXxx 是实时的,循环里改 DOM 会越改越多
- 坑 2:innerHTML 有 XSS 风险,用户输入要先转义
- 坑 3:循环里逐次操作 DOM 会触发多次回流,用
DocumentFragment批量插入 - 坑 4:事件回调里写匿名函数无法 removeEventListener,要用具名函数
- 坑 5:事件委托对 focus/blur 等不冒泡的事件无效,用 focusin/focusout
- 坑 6:
e.preventDefault()在passive: true的监听器里无效 - 坑 7:
setTimeout(0)不是 0ms,且回调不在同一次事件循环 - 坑 8:
history.pushState不会触发 popstate,只有用户前进后退才会触发 - 坑 9:
a.href = '#'会跳到页顶,用event.preventDefault()阻止 - 坑 10:直接修改
el.style触发回流,频繁修改可读requestAnimationFrame合并
10. 一句话总结
DOM 是"对页面的 API",BOM 是"对浏览器的 API"——会查节点、会改样式、会注册事件、会用事件委托,再加上 location/history 这俩"地址栏管家",就能操控一切前端行为。
11. 延伸阅读
- MDN:Document Object Model (DOM)
- MDN:Window
- 《JavaScript 高级程序设计》第 14 章 DOM