JS随笔:浏览器 API(DOM 与 BOM)

15 阅读11分钟

JS随笔:浏览器 API(DOM 与 BOM)

本篇是「JS随笔」系列中的浏览器 API 篇,面向浏览器端开发,系统梳理 DOM 与 BOM 的高频 API 与实践,包括元素选择、创建/插入/替换/删除、属性与样式、文本与事件、遍历与滚动、尺寸与位置、事件委托,以及 BOM 中的窗口、导航、历史、屏幕、存储、网络与并发能力。


原文地址

墨渊书肆/JS随笔:浏览器 API(DOM 与 BOM)


DOM 操作总览

获取元素

  • document.getElementById(id)
  • document.getElementsByTagName(name)
  • document.getElementsByClassName(names)
  • document.querySelector(selector)
  • document.querySelectorAll(selector)

实战中可以约定:

  • 读操作尽量使用 querySelector/querySelectorAll 统一选择器写法
  • 写操作前先判断元素是否存在,避免在 null 上访问属性导致运行时错误

创建与插入

  • document.createElement(tagName)
  • element.appendChild(node)
  • element.insertBefore(newNode, referenceNode)
  • element.replaceChild(newNode, oldNode)

删除元素

  • element.removeChild(node)
  • element.remove()

删除节点时要注意:

  • 事件监听器与引用:即便从 DOM 中移除,若还有 JS 引用或全局监听,仍然不会被回收
  • 频繁增删大量节点时,应尽量使用 DocumentFragment 批量操作,避免多次重排

属性操作

  • element.getAttribute(name)
  • element.setAttribute(name, value)
  • element.removeAttribute(name)

属性与 dataset 的常见边界:

  • getAttribute 返回字符串或 null,与直接读属性(如 input.checked)的语义不同
  • 自定义数据推荐使用 data-*element.dataset,避免与原生属性冲突

样式与类名

  • element.style.property
  • element.classList.add(className)
  • element.classList.remove(className)
  • element.classList.toggle(className)

实战中尽量避免从 JS 中频繁逐条设置行内样式,推荐:

  • 使用类名驱动样式变化,由 CSS 负责具体表现
  • 若确实需要多属性变更,可以合并到一次重绘中(如使用 requestAnimationFrame

文本

  • element.textContent
  • element.innerText

两者差异:

  • textContent 直接操作文本节点,不触发布局,性能更好
  • innerText 会考虑样式与布局(如 display:none),并触发回流,适合“真实可见文本”的场景

事件监听

  • element.addEventListener(type, listener[, options])
  • element.removeEventListener(type, listener[, options])

实践建议:

  • 使用具名函数或稳定引用,保证 removeEventListener 能正确解绑
  • 对滚动等高频事件添加 { passive: true } 与节流/防抖,避免滚动卡顿

遍历 DOM 树

  • element.parentNode
  • element.childNodes
  • element.firstChild
  • element.lastChild
  • element.nextSibling
  • element.previousSibling

滚动

  • window.scroll(x, y)
  • window.scrollTo(options)

尺寸与位置

  • element.offsetWidth / element.offsetHeight
  • element.getBoundingClientRect()

事件委托

事件委托利用事件冒泡原理在父级统一监听,减少事件绑定数量,提升性能。

var list = document.getElementById('myList');
list.addEventListener('click', function(event) {
  if (event?.target?.className === 'list-item') {
    event.target.textContent = 'Clicked!';
  }
});

优势:性能优化、代码简洁、兼容动态内容。

需要注意的边界包括:

  • 事件目标可能是子孙节点而非直接子节点,可结合 closest 判断:
list.addEventListener('click', e => {
  const item = e.target.closest('.list-item');
  if (!item || !list.contains(item)) return;
  // 处理 item
});
  • 有些事件不冒泡(如 blurfocus),此类事件无法直接使用传统事件委托,可改用 focusin/focusout 或单独绑定

BOM(浏览器对象模型)

核心对象

  • window:浏览器窗口与全局命名空间
  • locationURL、协议、主机名等
  • navigator:浏览器类型、版本与平台信息
  • history:前进/后退控制
  • screen:屏幕尺寸与颜色
  • document:DOM 顶级节点
  • console:调试输出
  • setTimeout / setInterval:定时器
  • fetch / XMLHttpRequest:网络请求
  • localStorage / sessionStorage:本地存储与会话存储
  • indexedDB:浏览器内置非关系型数据库
  • Web Workers:后台线程,避免阻塞 UI
  • 通知 API:系统通知

BOM 操作很容易引入全局副作用,实战中可以遵循两点:

  • 将与 URL、窗口、存储等相关操作抽象成服务层,避免在业务代码里散落 location.href/localStorage 等调用
  • 注意 SSR 或非浏览器环境下 window/document 不存在,模块初始化时避免直接访问

事件与并发实践

  • 合理拆分交互:将重计算逻辑迁移到 Web Worker
  • 优先微任务:在 UI 更新前完成关键微任务(如状态变更)
  • 队列与节流:为频繁事件(scrollresizemousemove)添加节流/防抖
  • 监听解绑:组件销毁时移除监听器,避免泄漏
  • 数据持久化:读频繁写稀疏场景使用 localStorage;大量结构化数据使用 indexedDB

典型坑位:

  • 在滚动事件中直接执行复杂计算或 DOM 操作 → 使用节流/防抖 + requestAnimationFrame
  • 忘记在单页应用路由切换时解除事件监听 → 统一封装注册/解绑逻辑,或使用框架生命周期钩子

存储与缓存

  • localStorage:持久化键值,容量有限且字符串化
  • sessionStorage:会话级存储
  • indexedDB:结构化数据与事务支持
  • 缓存策略:合理的过期、二级缓存(内存 + 本地)、版本化升级

做本地存储时要考虑:

  • 序列化格式:JSON 字符串是默认选择,必要时可考虑压缩或分段存储
  • 失效策略:为每条数据附带时间戳与版本号,避免长期使用脏数据
  • 容量上限:移动端浏览器容量较小,应避免在 localStorage 中存放大体积二进制数据

网络与安全

  • fetch:现代拉取 API;支持流与 AbortController
  • XMLHttpRequest:传统异步请求接口
  • 跨域与 CORS:服务端正确设置响应头
  • CSP 与 XSS:启用内容安全策略,避免注入
  • 资源指纹:哈希命名,避免缓存污染

常见安全边界:

  • 不信任来自 URL、表单、第三方接口的任何字符串,避免直接拼接到 innerHTML
  • 对需要插入 DOM 的内容,优先采用 textContent 或模板引擎的安全转义能力

DOM 进阶能力

  • 克隆与片段node.cloneNode(deep)DocumentFragment 批量插入减少重排
  • 数据属性element.dataset 读写 data-* 自定义数据
  • 计算样式getComputedStyle(element) 读取最终样式
  • 表单与校验form.checkValidity()input.reportValidity()
const frag = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  frag.appendChild(li);
}
document.querySelector('ul').appendChild(frag);

事件选项与观察者

  • 事件选项{ capture, once, passive } 优化监听行为与滚动性能
  • IntersectionObserver:懒加载与曝光统计
  • ResizeObserver:响应式布局观测容器变化
window.addEventListener('scroll', handler, { passive: true });

动画与绘制

  • requestAnimationFrame:与浏览器刷新同步的动画时钟
  • CSSOM:通过修改类名与 CSS 变量驱动动画
let last = performance.now();
function tick(now) {
  const dt = now - last;
  last = now;
  // 更新动画状态
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);

历史与路由

  • History APIpushState/replaceStatepopstate 事件实现前端路由
  • URLSearchParams:解析与生成查询字符串
history.pushState({ page: 2 }, '', '?page=2');
window.addEventListener('popstate', e => {
  // 读取 e.state
});
const params = new URLSearchParams(location.search);
params.get('page'); // '2'

Navigator 与权限

  • Navigatornavigator.userAgenthardwareConcurrencylanguage 等信息
  • Permissions API:查询权限状态(如通知、地理位置)
navigator.hardwareConcurrency; // 逻辑 CPU 数
navigator.language;

剪贴板与全屏

  • Clipboard API:读写剪贴板(需安全上下文与权限)
  • Fullscreen API:全屏展示与退出
await navigator.clipboard.writeText('hello');
document.documentElement.requestFullscreen();
document.exitFullscreen();

IndexedDB 示例

const req = indexedDB.open('my-db', 1);
req.onupgradeneeded = (e) => {
  const db = e.target.result;
  db.createObjectStore('items', { keyPath: 'id' });
};
req.onsuccess = () => {
  const db = req.result;
  const tx = db.transaction('items', 'readwrite');
  const store = tx.objectStore('items');
  store.put({ id: 1, name: 'A' });
  tx.oncomplete = () => db.close();
};

Web Worker 示例

// main.js
const worker = new Worker('worker.js');
worker.postMessage({ n: 100000 });
worker.onmessage = (e) => console.log('result', e.data);
 
// worker.js
self.onmessage = (e) => {
  const { n } = e.data;
  let sum = 0;
  for (let i = 0; i < n; i++) sum += i;
  postMessage(sum);
};

小结

  • 利用片段与观察者优化渲染与懒加载
  • 结合 History/API 与 URL 工具实现路由与状态管理
  • 通过权限、剪贴板与全屏提升交互能力
  • 使用 IndexedDB 与 Worker 管理数据与计算任务

MutationObserver 与 DOM 变更监听

  • 监听节点新增/删除/属性变更,避免轮询

相比早期的 DOMSubtreeModified 这类事件,MutationObserver 的优势在于:

  • 回调在批次中触发,可以合并多次变更,减少性能开销
  • 可精细配置监听范围(子节点、属性、文本等),避免无关变更造成干扰

典型应用包括:监听列表动态插入以做懒加载、监控第三方脚本插入的节点实现“安全包装”等。 需要注意控制观测范围与深度(subtree: true 时尤其要谨慎),否则在大 DOM 树上可能带来额外负担。

const target = document.querySelector('#app');
const obs = new MutationObserver(mutations => {
  for (const m of mutations) {
    if (m.type === 'childList') {
      // 子节点变化
    } else if (m.type === 'attributes') {
      // 属性变化
    }
  }
});
obs.observe(target, { childList: true, attributes: true, subtree: true });

IntersectionObserver 懒加载

  • 观测元素进入视口,触发加载或曝光上报

与手写滚动监听相比,IntersectionObserver 最大优势是:

  • 不需要手动计算元素位置与视口高度
  • 在浏览器层面做节流与合并回调,减少滚动抖动与性能问题

常见场景:图片懒加载、列表曝光埋点、无限滚动分页等。 需要注意的是,不同浏览器实现对阈值与根元素支持略有差异,尤其在嵌套滚动容器中,需要通过 rootrootMargin 合理配置。

const io = new IntersectionObserver(entries => {
  for (const e of entries) {
    if (e.isIntersecting) {
      // 加载图片/上报曝光
    }
  }
});
io.observe(document.querySelector('.card'));

ResizeObserver 响应式

  • 容器尺寸变化时调整布局或图表

ResizeObserver 更适合监听“容器尺寸”而非窗口尺寸,从而支持更细粒度的响应式布局。 例如图表组件可以根据容器宽度动态调整坐标轴密度,而无需监听全局 resize

在回调中应避免再次同步修改被观测元素的尺寸,容易造成无限回调; 可以通过微任务或动画帧调度,批量处理尺寸变更。

const chart = document.querySelector('#chart');
const ro = new ResizeObserver(entries => {
  for (const e of entries) {
    const { width, height } = e.contentRect;
    // 重绘图表
  }
});
ro.observe(chart);

PerformanceObserver 指标采样

  • 观测 FCP/LCP/CLS/长任务等性能条目

与单次 performance.getEntriesByType 相比,PerformanceObserver 更适合持续采样与上报:

  • 可以在用户整个使用周期内捕获关键指标
  • 支持监听长任务和布局偏移,帮助定位卡顿来源

实战中通常会将这些指标缓存在内存中,并以批量方式上报到监控平台, 避免在主线程频繁进行网络发送。

const po = new PerformanceObserver(list => {
  for (const e of list.getEntries()) {
    // 上报 e.name/e.startTime/e.duration
  }
});
po.observe({ entryTypes: ['largest-contentful-paint','longtask','layout-shift'] });

File API 与拖拽

  • 读取本地文件,配合拖拽上传

File API 提供了对用户本地文件的“受控访问”,常见应用如导入配置、上传图片与文档等。 拖拽场景下需要注意:

  • 明确阻止默认行为,避免浏览器直接在当前页打开文件
  • 对大文件采用分片上传或流式处理,避免一次性读入占满内存
const input = document.querySelector('input[type=file]');
input.addEventListener('change', async () => {
  const file = input.files[0];
  const text = await file.text();
});
document.addEventListener('drop', e => {
  e.preventDefault();
  for (const file of e.dataTransfer.files) {
    // 处理 file
  }
});
document.addEventListener('dragover', e => e.preventDefault());

Canvas 概览

  • 2D 绘制接口,适合图形/图表/像素处理

Canvas 更偏底层绘图接口,适合需要逐像素控制或高频重绘的场景:

  • 自定义图表与可视化
  • 简单小游戏与粒子特效

与 DOM/CSS 渲染相比,Canvas 中的元素不再是可独立选中的节点,需要自行管理绘制状态与交互区域, 因此更适合作为“内部引擎”,对外暴露封装良好的组件接口。

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#f66';
ctx.fillRect(10, 10, 100, 50);

WebRTC(简述)

  • 实时音视频与数据通道

WebRTC 的核心价值在于提供“点对点”的实时传输能力,包括音视频与任意二进制数据通道。 典型场景有:音视频会议、屏幕共享、实时协作编辑等。

需要注意:

  • 信令与房间管理不在 WebRTC 规范中,需要自行设计或使用现成服务
  • 网络环境复杂时,候选地址收集与连通性测试可能较慢,需要良好的超时与重试策略
const pc = new RTCPeerConnection();
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
stream.getTracks().forEach(t => pc.addTrack(t, stream));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 通过信令服务器交换 SDP 与 candidate

Clipboard 与共享

  • 读取富媒体或图片

剪贴板 API 在现代浏览器中通常要求安全上下文与显式用户交互触发, 以避免恶意页面在后台悄悄读取用户数据。

在跨平台分享场景下,可优先尝试 Web Share API,在不支持的环境中回退到剪贴板写入, 从而同时兼顾移动端与桌面端体验。

const items = await navigator.clipboard.read();
for (const item of items) {
  for (const type of item.types) {
    const blob = await item.getType(type);
    // 处理 blob
  }
}
  • Web Share API
await navigator.share({ title: '标题', text: '内容', url: location.href });

通知与权限

  • 请求权限并发送通知

通知 API 适合用于少量“高价值提醒”,例如长任务完成、重要消息到达等。 实战中需要避免频繁或无意义通知,以免被用户直接关闭权限。

同时要考虑权限状态:

  • 初次访问应给出清晰的使用理由,再触发授权请求
  • 对被拒绝的权限保持静默降级行为,避免每次都重复弹窗
if (Notification.permission !== 'granted') {
  await Notification.requestPermission();
}
new Notification('提示', { body: '任务完成' });

2025/2026 相关更新与提示

  • Intl.Locale 变体(ES2026 预计):更细粒度的区域设置,配合 Intl.DateTimeFormat/Intl.NumberFormat 优化本地化呈现
const locale = new Intl.Locale('zh-CN', { calendar: 'gregory', numberingSystem: 'hanidec' });
const nf = new Intl.NumberFormat(locale.baseName);
nf.format(123456); // 本地化数字
  • import defer(语言层):对于以模块方式加载的前端应用,延迟求值非关键模块以提升首屏稳定性(与 <script type="module"> 结合使用)

实战清单

  • 使用 Observer 系列减少手工轮询
  • 对频繁事件应用节流/防抖与 passive 优化滚动
  • 结合 PerformanceObserver 持续采样与上报关键指标
  • 按需引入 Clipboard/File/Canvas/WebRTC 提升交互能力

存储策略补充

  • 分层缓存:内存(Map)→ sessionStorage → localStorage → indexedDB
  • 版本化:为结构性数据维护 schemaVersion 与迁移脚本
  • 写入合并:批量写入 indexedDB,减少事务开销
const mem = new Map();
function getCached(key) {
  if (mem.has(key)) return mem.get(key);
  const v = localStorage.getItem(key);
  if (v != null) mem.set(key, v);
  return v;
}

URL 与导航补充

  • 使用 URL 对象安全拼接路径与参数
const url = new URL('/search', location.origin);
url.searchParams.set('q', 'js');
history.pushState({}, '', url);