Skip to content

第 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注释节点
9document 节点

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

维度NodeListHTMLCollection
来源querySelectorAll、childNodesgetElementsBy*
实时性静态快照(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
                    ▲                   │
                    │  (从内向外)        │
                    └── 冒泡阶段 ────────┘
  1. 捕获阶段:从 window 向目标传递
  2. 目标阶段:到达事件目标
  3. 冒泡阶段:从目标向 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 / prompt

7.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. 延伸阅读