JavaScript DOM 与 BOM 深度指南

2024-09-03 前端

一、DOM 基础

DOM(Document Object Model,文档对象模型)是 JavaScript 操作 HTML 和 XML 文档的编程接口。它将网页文档解析为树形结构,使开发者能够动态地访问和修改页面内容、结构和样式。

1.1 DOM 树结构

DOM 树以 document 为根节点,包含以下几类节点:

节点类型 NodeType 值 描述
Document 9 整个文档的根节点
Element 1 HTML 元素节点
Attr 2 元素的属性节点(已弃用,建议用 getAttribute)
Text 3 元素中的文本内容
Comment 8 HTML 注释节点
DocumentFragment 11 轻量文档片段
// 检查节点类型
const div = document.createElement('div');
console.log(div.nodeType);        // 1 (Element)
console.log(div.nodeName);        // "DIV"

const text = document.createTextNode('Hello');
console.log(text.nodeType);       // 3 (Text)

const comment = document.createComment('注释');
console.log(comment.nodeType);    // 8 (Comment)

console.log(document.nodeType);   // 9 (Document)
console.log(DocumentFragment.prototype.nodeType); // 11

1.2 Node 接口 vs Element 接口

Node 是所有 DOM 节点类型的基接口,而 Element 继承自 Node,专注于 HTML 元素的操作。

// Node 接口提供的属性和方法(所有节点都可用)
const el = document.querySelector('div');

// Node 层面的属性
el.nodeType;       // 1
el.nodeName;       // "DIV"
el.parentNode;     // 父节点(可能是 Element 或 DocumentFragment)
el.childNodes;     // NodeList(包含所有子节点,含文本和注释)
el.firstChild;     // 第一个子节点(可能是文本节点)
el.lastChild;      // 最后一个子节点
el.previousSibling;
el.nextSibling;
el.textContent;    // 所有后代文本内容

// Element 层面的属性(仅 Element 节点可用)
el.tagName;        // "DIV"(类似 nodeName)
el.id;
el.className;
el.children;       // HTMLCollection(仅包含元素子节点)
el.firstElementChild;
el.lastElementChild;
el.previousElementSibling;
el.nextElementSibling;
el.innerHTML;
el.outerHTML;

关键区别:childNodes 包含文本节点和注释节点,而 children 只包含元素节点。

// childNodes vs children 的差异
const ul = document.querySelector('ul');
// HTML: <ul><li>A</li><li>B</li></ul>

console.log(ul.childNodes.length);  // 5: [text, li, text, li, text]
// 两个 <li> 之间的空白字符会被解析为 text 节点
console.log(ul.children.length);    // 2: [li, li]

1.3 节点导航

节点导航分为 Node 版本 和 Element 版本,两者行为不同:

// Node 版导航(包含文本和注释)
const container = document.getElementById('container');

container.parentNode;           // 父节点
container.childNodes;           // 所有子节点(NodeList)
container.firstChild;           // 第一个子节点
container.lastChild;            // 最后一个子节点
container.previousSibling;      // 前一个兄弟节点
container.nextSibling;          // 后一个兄弟节点

// Element 版导航(仅包含 Element 节点)
container.parentElement;              // 父元素(必须是 Element,否则返回 null)
container.children;                   // 所有子元素(HTMLCollection,实时集合)
container.firstElementChild;          // 第一个子元素
container.lastElementChild;           // 最后一个子元素
container.previousElementSibling;     // 前一个兄弟元素
container.nextElementSibling;         // 后一个兄弟元素
// 实战:遍历纯元素节点(跳过文本与注释)
function walkElementChildren(element, callback) {
  let child = element.firstElementChild;
  while (child) {
    callback(child);
    child = child.nextElementSibling;
  }
}

1.4 查询方法

经典查询方法

// 1. getElementById — 最快,返回单个元素或 null
const header = document.getElementById('header');

// 2. getElementsByClassName — 返回 HTMLCollection(实时集合)
const items = document.getElementsByClassName('item');
console.log(items.length);  // 当 DOM 变化时自动更新

// 3. getElementsByTagName — 返回 HTMLCollection(实时集合)
const paragraphs = document.getElementsByTagName('p');

现代查询方法(CSS 选择器)

// 4. querySelector — 返回匹配的第一个元素或 null
const firstBtn = document.querySelector('.btn.primary');
const navItem = document.querySelector('#nav > li:first-child');

// 5. querySelectorAll — 返回静态 NodeList
const allBtns = document.querySelectorAll('.btn');
console.log(allBtns.length);   // 静态快照,DOM 变化不影响它
console.log(allBtns[0]);       // 支持 forEach,但不支持 map
allBtns.forEach(btn => btn.classList.add('active'));

// 6. closest — 从当前元素向上查找最近的匹配祖先(含自身)
const card = event.target.closest('.card');
if (card) {
  console.log('点击发生在卡片内:', card.id);
}

// 7. matches — 检查元素是否匹配某个选择器
function isButton(el) {
  return el.matches('button, [role="button"], input[type="submit"]');
}

1.5 实时集合 vs 静态集合

特性 HTMLCollection(实时) NodeList(querySelectorAll)
更新方式 DOM 变化自动反映 静态快照
索引访问 ✅ ✅
forEach ❌ ✅
长度变化 随 DOM 自动变化 固定
// 实时集合的陷阱
const liveItems = document.getElementsByClassName('item');
console.log('初始数量:', liveItems.length); // 3

// 移除一个元素
liveItems[0].remove();
console.log('移除后数量:', liveItems.length); // 2(自动更新)

// 静态集合不受影响
const staticItems = document.querySelectorAll('.item');
console.log('静态集合:', staticItems.length); // 始终是查询时的快照

二、节点操作

2.1 创建节点

// 1. 创建元素节点
const div = document.createElement('div');
div.id = 'new-div';
div.className = 'box';

// 2. 创建文本节点
const text = document.createTextNode('Hello World');

// 3. 创建注释节点
const comment = document.createComment('这是注释');

// 4. 创建文档片段(批量操作优化)
const fragment = document.createDocumentFragment();

2.2 插入节点

传统方法

const parent = document.getElementById('list');
const newItem = document.createElement('li');
newItem.textContent = '新增项';

// appendChild — 在末尾追加
parent.appendChild(newItem);

// insertBefore — 在指定子节点前插入
const referenceItem = parent.children[0];
parent.insertBefore(newItem, referenceItem);

现代方法(更直观)

const parent = document.getElementById('list');
const item = document.createElement('li');
item.textContent = '新项';

// append — 在末尾追加(支持多个参数和字符串)
parent.append(item);
parent.append('直接文本', document.createElement('span'));

// prepend — 在开头插入
parent.prepend(item);

// after — 在当前元素之后插入兄弟元素
const current = document.getElementById('current');
current.after(document.createElement('hr'));

// before — 在当前元素之前插入兄弟元素
current.before(document.createElement('hr'));

传统 vs 现代对比:

方法 返回值 多节点支持 字符串处理
appendChild 插入的节点 ❌ ❌
insertBefore 插入的节点 ❌ ❌
append undefined ✅ 自动转文本节点
prepend undefined ✅ 自动转文本节点
after undefined ✅ 自动转文本节点
before undefined ✅ 自动转文本节点

2.3 替换节点

const old = document.getElementById('old');
const newEl = document.createElement('div');
newEl.textContent = '新元素';

// replaceChild — 父元素调用,替换某个子节点
old.parentNode.replaceChild(newEl, old);

// replaceWith — 元素自身调用(更简洁)
old.replaceWith(newEl);

// replaceChildren — 替换所有子节点
const container = document.getElementById('container');
container.replaceChildren(
  document.createElement('h2'),
  document.createElement('p')
);

2.4 删除节点

const element = document.getElementById('to-remove');

// removeChild — 父元素调用删除某个子节点,返回被删除的节点
const removed = element.parentNode.removeChild(element);

// remove — 元素自身删除(更简洁,无返回值)
element.remove();

// 清空所有子节点
const parent = document.getElementById('parent');

// 方法1:逐个删除
while (parent.firstChild) {
  parent.removeChild(parent.firstChild);
}

// 方法2:直接清空 innerHTML(有内存泄漏风险)
parent.innerHTML = '';

// 方法3:replaceChildren(推荐)
parent.replaceChildren();

2.5 cloneNode

const original = document.getElementById('original');

// 浅克隆 — 仅复制元素本身,不包含子节点
const shallowClone = original.cloneNode(false);

// 深克隆 — 复制元素及其所有后代
const deepClone = original.cloneNode(true);

// 注意事项:
// 1. 克隆不会复制事件监听器
// 2. 克隆不会复制 data 属性(绑定在 JS 对象上的数据)
// 3. id 属性会被复制,可能导致重复 id
const clone = original.cloneNode(true);
clone.id = original.id + '-clone'; // 记得修改 id

2.6 DocumentFragment 批量操作优化

DocumentFragment 是一个轻量级的文档对象,存在于内存中而非真实 DOM 树。批量插入时使用它可避免多次触发浏览器重排。

// ❌ 低效方式:每次 appendChild 都触发重排
const list = document.getElementById('list');
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `项 ${i}`;
  list.appendChild(li); // 每次操作都触发 reflow
}

// ✅ 高效方式:使用 DocumentFragment 批量插入
const list = document.getElementById('list');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `项 ${i}`;
  fragment.appendChild(li); // 操作在内存中完成
}
list.appendChild(fragment); // 仅触发一次 reflow

// ✅ 现代方式:append 也支持批量(但仍推荐 fragment 避免大数组)
list.append(...items); // 参数展开可能爆栈,适合中量操作

三、元素属性与内容

3.1 innerHTML vs outerHTML vs textContent vs innerText

const el = document.getElementById('demo');
// 假设 HTML: <div id="demo">Hello <b>World</b></div>

// innerHTML — 获取/设置元素的 HTML 内容(会解析 HTML 字符串)
console.log(el.innerHTML);         // "Hello <b>World</b>"
el.innerHTML = '<span>新内容</span>'; // 解析并替换 HTML

// outerHTML — 包含元素自身的 HTML
console.log(el.outerHTML);
// "<div id="demo">Hello <b>World</b></div>"
el.outerHTML = '<section>替换</section>'; // 用新元素替换自身

// textContent — 所有文本内容(忽略标签,包含隐藏元素的文本)
console.log(el.textContent);       // "Hello World"
el.textContent = '纯文本';         // 设置纯文本,不会解析 HTML 标签

// innerText — 可见文本(考虑了 CSS 样式,性能较慢)
console.log(el.innerText);         // "Hello World"(不包含 display:none 的文本)
el.innerText = '可见文本';         // 设置可见文本

关键差异总结:

属性 解析 HTML 性能 包含隐藏文本 返回值
innerHTML ✅ 较慢(需解析) ✅ HTML 字符串
outerHTML ✅ 较慢 ✅ 包含自身的 HTML
textContent ❌ 快 ✅ 纯文本
innerText ❌ 慢(触发布局) ❌ 可见纯文本
// 实战:安全地设置纯文本(防止 XSS)
function setTextSafely(element, text) {
  element.textContent = text; // 自动转义 HTML 特殊字符
  // ❌ element.innerHTML = text; // 不安全!
}

// 实战:获取所有可见文本
function getVisibleText(element) {
  // innerText 会自动忽略 display:none 的子元素文本
  return element.innerText.trim();
}

3.2 getAttribute / setAttribute / removeAttribute

const link = document.querySelector('a');

// 获取属性
console.log(link.getAttribute('href'));
console.log(link.getAttribute('target'));

// 设置属性
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');

// 删除属性
link.removeAttribute('target');

// 检查属性是否存在
console.log(link.hasAttribute('href')); // true

// 获取所有属性
const attrs = link.attributes; // NamedNodeMap
for (const attr of attrs) {
  console.log(`${attr.name}: ${attr.value}`);
}

属性(Attribute)vs 属性(Property)的区别:

const input = document.querySelector('input[type="checkbox"]');

// Attribute 是 HTML 上的初始值
input.getAttribute('checked'); // ""(存在即返回空字符串)

// Property 是 DOM 对象的当前状态
input.checked; // false(未选中时)

// 两者不同步的场景:
input.setAttribute('value', '初始值');
console.log(input.value); // ""(如果用户已输入内容,property 不同步)

input.value = '用户输入';
console.log(input.getAttribute('value')); // "初始值"(attribute 不变)

3.3 dataset(data-* 属性)

HTML5 提供的 data-* 自定义属性,通过 dataset API 便捷访问:

// HTML: <div id="card" data-id="123" data-type="article" data-user-name="张三"></div>

const card = document.getElementById('card');

// 读取 data-* 属性
console.log(card.dataset.id);       // "123"
console.log(card.dataset.type);     // "article"
console.log(card.dataset.userName); // "张三"(驼峰转换)

// 设置 data-* 属性
card.dataset.status = 'active';
// HTML 变为: <div ... data-status="active"></div>

card.dataset.userAge = '28';
// HTML 变为: <div ... data-user-age="28"></div>

// 删除 data-* 属性
delete card.dataset.type;

// 判断是否存在
console.log('id' in card.dataset); // true

// 遍历所有 data-* 属性
for (const key in card.dataset) {
  console.log(`data-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${card.dataset[key]}`);
}

命名转换规则:

HTML(data-*) dataset 属性
data-user-name dataset.userName
data-id dataset.id
data-json-data dataset.jsonData

3.4 classList

classList 提供了比 className 更强大、更安全的类操作:

const el = document.getElementById('element');

// classList API
el.classList.add('active', 'highlight');  // 添加一个或多个类
el.classList.remove('inactive');           // 移除一个类
el.classList.toggle('visible');            // 切换类(存在则删除,不存在则添加)
el.classList.contains('active');           // 检查是否包含某个类 → boolean
el.classList.replace('old', 'new');        // 替换类名

// 批量操作
el.classList.add(...['class1', 'class2']);

// 遍历 classList
el.classList.forEach(cls => console.log(cls));

// 获取类名数量
console.log(el.classList.length);

// className 方式(字符串操作,较原始)
el.className = 'active highlight';         // 覆盖所有类名
el.className += ' new-class';              // 追加(注意空格)
const hasClass = el.className.includes('active'); // 不精确,可能误匹配

实战:常见 class 操作模式:

// 1. 条件性添加类
const isDisabled = true;
el.classList.toggle('disabled', isDisabled); // 第二个参数强制添加/移除

// 2. 删除多个指定类
el.classList.remove('btn', 'btn-primary', 'btn-lg');

// 3. 复合状态管理
function updateState(el, state) {
  el.classList.remove('loading', 'error', 'success');
  if (state) el.classList.add(state);
}

// 4. className 与 classList 的性能选择
// className 在需要完全重写时更快(避免多次 DOM 写入)
// classList 在需要部分修改时更安全

四、样式操作

4.1 style 属性(行内样式)

通过 element.style 操作行内样式,对应 HTML 的 style 属性:

4.2 getComputedStyle(计算样式)

getComputedStyle 获取最终生效的样式值(包含所有 CSS 来源:行内、样式表、继承等):

const element = document.getElementById('my-element');

// 获取计算样式
const computed = window.getComputedStyle(element);

// 读取计算后的属性值
console.log(computed.display);          // "flex"(实际生效的值)
console.log(computed.width);            // "960px"(含单位的像素值)
console.log(computed.fontSize);         // "16px"
console.log(computed.backgroundColor);  // "rgb(52, 152, 219)"

// 使用 getComputedStyle 获取伪元素样式
const pseudoStyle = window.getComputedStyle(element, '::before');
console.log(pseudoStyle.content);
console.log(pseudoStyle.color);

// 获取 CSS 自定义属性的计算值
const root = document.documentElement;
const rootStyle = getComputedStyle(root);
const primaryColor = rootStyle.getPropertyValue('--primary-color').trim();
console.log(primaryColor); // "#3498db"

// getComputedStyle vs element.style
// element.style       → 仅读取或设置行内样式(可写)
// getComputedStyle    → 读取最终计算样式(只读)

4.3 className vs classList

场景 className classList
完全重写类名 ✅ 简单直接 ❌ 需要先清除再添加
追加单个类 el.className += ' cls'(易出错) ✅ el.classList.add('cls')
删除单个类 需要字符串替换 ✅ el.classList.remove('cls')
切换类 需要手动判断 ✅ el.classList.toggle('cls')
检查是否包含 el.className.includes('cls')(可能误匹配) ✅ el.classList.contains('cls')
批量类名 ✅ 字符串一次写入 需要逐个添加
// className 适合完全重写的场景
function setTheme(theme) {
  document.body.className = `theme-${theme}`;
}

// classList 适合部分修改的场景
function toggleNav() {
  document.querySelector('.nav').classList.toggle('nav--open');
}

4.4 CSS 自定义属性(CSS 变量)的 JS 操作

CSS 变量通过 -- 前缀定义,JS 中通过 setProperty 和 getPropertyValue 操作:

// 设置 CSS 变量
const root = document.documentElement;

// 设置全局变量
root.style.setProperty('--primary', '#3498db');
root.style.setProperty('--spacing-unit', '8px');
root.style.setProperty('--font-size-base', '16px');

// 获取 CSS 变量
const primary = getComputedStyle(root).getPropertyValue('--primary').trim();
console.log(primary); // "#3498db"

// 响应式主题切换
function setTheme(themeName) {
  const root = document.documentElement;
  if (themeName === 'dark') {
    root.style.setProperty('--bg', '#1a1a2e');
    root.style.setProperty('--text', '#eee');
    root.style.setProperty('--primary', '#e94560');
  } else {
    root.style.setProperty('--bg', '#fff');
    root.style.setProperty('--text', '#333');
    root.style.setProperty('--primary', '#3498db');
  }
}

// 在元素级别覆盖变量
const card = document.querySelector('.card');
card.style.setProperty('--primary', '#2ecc71');

五、事件机制

5.1 事件流

DOM 事件流分为三个阶段:

// 1. 捕获阶段监听(第三个参数传 true)
document.getElementById('outer').addEventListener('click', (e) => {
  console.log('捕获阶段 - outer');
}, true); // capture: true

// 2. 冒泡阶段监听(第三个参数 false 或省略)
document.getElementById('inner').addEventListener('click', (e) => {
  console.log('冒泡阶段 - inner');
}, false);

// 3. 仅捕获阶段执行,冒泡阶段忽略
document.addEventListener('click', handler, { capture: true });

5.2 addEventListener 选项

addEventListener 的第三个参数可以是布尔值或选项对象:

事件监听器移除的三种方式:

// 方式1:removeEventListener(需要同一个函数引用)
function handleClick() { console.log('click'); }
btn.addEventListener('click', handleClick);
btn.removeEventListener('click', handleClick); // 必须传入同一个函数

// 方式2:once 选项(自动移除)
btn.addEventListener('click', () => console.log('仅一次'), { once: true });

// 方式3:AbortController(批量移除)
const ac = new AbortController();
btn.addEventListener('click', handler, { signal: ac.signal });
ac.abort(); // 移除 signal 绑定的所有监听器

5.3 事件对象

5.4 事件委托

事件委托利用事件冒泡机制,在父元素上统一处理子元素的事件,减少监听器数量:

5.5 自定义事件

使用 CustomEvent 创建和派发自定义事件:

5.6 常见事件分类

鼠标事件

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('contextmenu', handler);  // 右键菜单

// 事件对象属性
element.addEventListener('click', (e) => {
  console.log(e.clientX, e.clientY);  // 视口坐标
  console.log(e.pageX, e.pageY);      // 页面坐标
  console.log(e.screenX, e.screenY);  // 屏幕坐标
  console.log(e.button);              // 0=左键, 1=中键, 2=右键
  console.log(e.ctrlKey, e.shiftKey, e.altKey, e.metaKey); // 修饰键
});

键盘事件

document.addEventListener('keydown', (e) => {
  console.log(e.key);       // "a", "Enter", "ArrowUp"
  console.log(e.code);      // "KeyA", "Enter", "ArrowUp"
  console.log(e.keyCode);   // 已弃用,不推荐使用
  console.log(e.repeat);    // 是否长按重复触发

  // 组合键判断
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault();
    console.log('保存快捷键');
  }
  if (e.metaKey && e.key === 'k') {
    e.preventDefault();
    console.log('Mac 的 Cmd+K');
  }
});

document.addEventListener('keyup', handler);
document.addEventListener('keypress', handler); // 已弃用,用 keydown 替代

表单事件

const form = document.querySelector('form');
const input = document.querySelector('input');

form.addEventListener('submit', (e) => {
  e.preventDefault(); // 阻止表单提交刷新页面
  const formData = new FormData(form);
  console.log([...formData.entries()]);
});

input.addEventListener('focus', () => console.log('获得焦点'));
input.addEventListener('blur', () => console.log('失去焦点'));
input.addEventListener('input', (e) => console.log(e.target.value)); // 值变化
input.addEventListener('change', (e) => console.log('值已确认'));    // 失焦且值变化
input.addEventListener('invalid', (e) => console.log('验证失败'));

// 输入防抖(debounce)
let debounceTimer;
input.addEventListener('input', () => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    console.log('发送搜索请求:', input.value);
  }, 300);
});

焦点事件

element.addEventListener('focus', handler);    // 获得焦点(不冒泡)
element.addEventListener('blur', handler);     // 失去焦点(不冒泡)
element.addEventListener('focusin', handler);  // 获得焦点(冒泡)
element.addEventListener('focusout', handler); // 失去焦点(冒泡)

// 焦点管理示例
document.addEventListener('focusin', (e) => {
  console.log('当前焦点元素:', e.target);
});

document.addEventListener('focusout', (e) => {
  console.log('焦点移出:', e.target);
  console.log('移入:', e.relatedTarget); // 新获得焦点的元素
});

滚动与触摸事件

// 滚动事件
window.addEventListener('scroll', () => {
  console.log('滚动位置:', window.scrollY);
}, { passive: true }); // 必须加 passive 提升性能

// 触摸事件(移动端)
element.addEventListener('touchstart', (e) => {
  console.log('触摸点:', e.touches);
  console.log('触摸目标:', e.targetTouches);
  console.log('已改变的触摸:', e.changedTouches);
});

element.addEventListener('touchmove', (e) => {
  // 阻止页面滚动
  e.preventDefault();
}, { passive: false });

element.addEventListener('touchend', (e) => {
  console.log('触摸结束');
  // 判断滑动方向
  const touch = e.changedTouches[0];
  console.log('结束位置:', touch.clientX, touch.clientY);
});

拖拽事件


六、BOM(浏览器对象模型)

BOM(Browser Object Model)提供与浏览器本身交互的对象,核心是 window 对象。

6.1 window 对象

window 是 BOM 的顶层对象,代表浏览器窗口或标签页:

6.2 navigator 对象

navigator 提供浏览器和操作系统信息:

6.3 location 对象

location 对象管理当前页面的 URL:

6.4 history 对象

history 管理浏览器的会话历史(前进/后退):

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

// 当前历史栈长度
console.log(history.length);

// HTML5 History API(SPA 路由核心)

// pushState — 添加新的历史记录条目
const state = { page: 'home', scrollY: window.scrollY };
history.pushState(state, '', '/home');

// replaceState — 替换当前历史记录条目
history.replaceState({ page: 'about' }, '', '/about');

// popstate 事件 — 用户点击前进/后退按钮时触发
window.addEventListener('popstate', (e) => {
  console.log('状态变化:', e.state); // { page: 'home', ... }
  // 根据 state 更新页面内容
  if (e.state) {
    loadPage(e.state.page);
  }
});

6.5 screen 对象

screen 提供用户屏幕的信息:

console.log(screen.width);            // 屏幕总宽度(像素)
console.log(screen.height);           // 屏幕总高度
console.log(screen.availWidth);       // 可用宽度(排除任务栏等)
console.log(screen.availHeight);      // 可用高度
console.log(screen.colorDepth);       // 色彩深度(通常 24 或 32)
console.log(screen.orientation);      // ScreenOrientation 对象

// 屏幕方向
if (screen.orientation) {
  console.log(screen.orientation.type); // "landscape-primary", "portrait-primary"

  // 锁定屏幕方向
  screen.orientation.lock('portrait')
    .then(() => console.log('锁定竖屏'))
    .catch(err => console.error(err));

  // 解除锁定
  screen.orientation.unlock();

  // 方向变化监听
  screen.orientation.addEventListener('change', () => {
    console.log('屏幕方向变为:', screen.orientation.type);
  });
}

// 检测是否为 Retina 屏幕
const isRetina = window.devicePixelRatio >= 2;
console.log('设备像素比:', window.devicePixelRatio); // 1, 2, 3

6.6 localStorage / sessionStorage

Web Storage 提供客户端键值存储:


七、Intersection Observer

IntersectionObserver 异步观察目标元素与视口(或指定容器)的交叉状态,性能远优于 scroll 事件监听。

7.1 基本用法

// 创建观察器
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    // entry 包含每个被观察元素的交叉信息
    console.log('目标元素:', entry.target);
    console.log('是否交叉:', entry.isIntersecting);
    console.log('交叉比例:', entry.intersectionRatio);
    console.log('交叉矩形:', entry.intersectionRect);
    console.log('目标边界:', entry.boundingClientRect);
    console.log('根边界:', entry.rootBounds);
  });
}, {
  root: null,           // 默认为视口(浏览器可视区域)
  rootMargin: '0px',    // 扩大或缩小根边界
  threshold: [0, 0.5, 1] // 触发回调的交叉比例阈值
});

// 开始观察
const target = document.getElementById('lazy-image');
observer.observe(target);

// 停止观察
observer.unobserve(target);

// 断开所有观察
observer.disconnect();

7.2 懒加载图片实现

7.3 无限滚动加载

7.4 曝光统计


八、MutationObserver

MutationObserver 监听 DOM 树的变化,是替代已弃用的 MutationEvent 的现代方案。

8.1 基本用法

8.2 实际应用


九、ResizeObserver

ResizeObserver 观察元素尺寸变化,比 window.resize 事件更精确。

9.1 基本用法

// 创建观察器
const observer = new ResizeObserver((entries) => {
  entries.forEach(entry => {
    const { target, contentRect, borderBoxSize, contentBoxSize } = entry;

    console.log('目标元素:', target);
    console.log('内容矩形:', contentRect);
    // contentRect: { x, y, width, height, top, right, bottom, left }

    // 新版 API:获取精确的像素值
    if (contentBoxSize) {
      const inlineSize = contentBoxSize[0].inlineSize; // 宽度
      const blockSize = contentBoxSize[0].blockSize;   // 高度
      console.log(`尺寸: ${inlineSize} x ${blockSize}`);
    }
  });
});

// 开始观察
const element = document.getElementById('resizable');
observer.observe(element);

// 停止观察
observer.unobserve(element);

// 断开所有
observer.disconnect();

9.2 响应式组件

9.3 ResizeObserver 与 window.resize 对比

特性 ResizeObserver window.resize
精度 监听单个元素尺寸变化 仅监听窗口变化
性能 高效,批量处理 频繁触发
应用场景 容器查询、响应式组件 全局布局调整
是否支持 iframe ✅ ❌

十、DOM 性能优化

10.1 重排(Reflow)与重绘(Repaint)

重排(Reflow/Layout):浏览器重新计算元素的位置和几何尺寸。代价高昂。
重绘(Repaint):元素视觉样式改变但布局不变时的重新绘制。代价较低。

// 触发重排的操作
// 1. 修改几何属性
element.style.width = '200px';
element.style.height = '100px';
element.style.padding = '20px';
element.style.margin = '10px';
element.style.borderWidth = '2px';

// 2. 修改 DOM 树
element.appendChild(newChild);
element.removeChild(child);
element.innerHTML = '<div>新内容</div>';

// 3. 修改类名(如果类名影响布局)
element.className = 'new-layout';

// 4. 访问布局属性(强制同步布局)
const height = element.offsetHeight;     // 强制触发重排
const width = element.offsetWidth;       // 强制触发重排
const rect = element.getBoundingClientRect(); // 强制触发重排
const computed = getComputedStyle(element).height; // 强制触发重排

// 仅触发重绘(不改变布局)
element.style.color = 'red';
element.style.backgroundColor = '#eee';
element.style.visibility = 'hidden';    // display:none 会触发重排
element.style.borderColor = 'blue';

10.2 批量 DOM 操作

10.3 离线 DOM 操作

将元素从文档流中移除进行操作,完成后放回:

10.4 requestAnimationFrame 进行 DOM 更新

requestAnimationFrame(rAF)在浏览器下一次重绘之前执行回调,是动画和批量 DOM 更新的最佳选择。

10.5 完整优化清单


总结

本文全面介绍了 JavaScript DOM 和 BOM 的核心知识体系:

  • DOM 基础:涵盖了 DOM 树结构、Node 与 Element 接口的区别、节点导航的两种模式、五种核心查询方法及其实时/静态集合特性。
  • 节点操作:从创建、插入、替换到删除,对比了传统方法与现代 API(append、prepend、after、before)的差异;重点介绍了 DocumentFragment 的批量操作优化。
  • 属性与内容:深入剖析了 innerHTML、outerHTML、textContent、innerText 四者的差异,以及 dataset、classList 的现代用法。
  • 样式操作:覆盖行内样式、计算样式、CSS 变量的 JS 读写,以及 className 与 classList 的适用场景。
  • 事件机制:完整解释了事件流的三个阶段、addEventListener 的高级选项(once、passive、signal)、事件委托模式、自定义事件及常见事件分类。
  • BOM:系统介绍了 window、navigator、location、history、screen 以及 Web Storage 的完整 API,涵盖地理定位、媒体设备、剪贴板等实用能力。
  • 三大 Observer:IntersectionObserver(懒加载/无限滚动/曝光统计)、MutationObserver(DOM 变化监听)、ResizeObserver(元素尺寸响应),每个都配有可直接运行的实战代码。
  • 性能优化:从重排重绘的原理出发,总结了批量操作、离线 DOM、读写分离、requestAnimationFrame 等关键优化策略。

掌握这些知识,你将能够高效地操纵 DOM、利用浏览器能力、编写高性能的前端交互代码。