JS随笔:浏览器 API(DOM 与 BOM)
本篇是「JS随笔」系列中的浏览器 API 篇,面向浏览器端开发,系统梳理 DOM 与 BOM 的高频 API 与实践,包括元素选择、创建/插入/替换/删除、属性与样式、文本与事件、遍历与滚动、尺寸与位置、事件委托,以及 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.propertyelement.classList.add(className)element.classList.remove(className)element.classList.toggle(className)
实战中尽量避免从 JS 中频繁逐条设置行内样式,推荐:
- 使用类名驱动样式变化,由 CSS 负责具体表现
- 若确实需要多属性变更,可以合并到一次重绘中(如使用
requestAnimationFrame)
文本
element.textContentelement.innerText
两者差异:
textContent直接操作文本节点,不触发布局,性能更好innerText会考虑样式与布局(如display:none),并触发回流,适合“真实可见文本”的场景
事件监听
element.addEventListener(type, listener[, options])element.removeEventListener(type, listener[, options])
实践建议:
- 使用具名函数或稳定引用,保证
removeEventListener能正确解绑 - 对滚动等高频事件添加
{ passive: true }与节流/防抖,避免滚动卡顿
遍历 DOM 树
element.parentNodeelement.childNodeselement.firstChildelement.lastChildelement.nextSiblingelement.previousSibling
滚动
window.scroll(x, y)window.scrollTo(options)
尺寸与位置
element.offsetWidth/element.offsetHeightelement.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
});
- 有些事件不冒泡(如
blur、focus),此类事件无法直接使用传统事件委托,可改用focusin/focusout或单独绑定
BOM(浏览器对象模型)
核心对象
- window:浏览器窗口与全局命名空间
- location:
URL、协议、主机名等 - 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 更新前完成关键微任务(如状态变更)
- 队列与节流:为频繁事件(
scroll、resize、mousemove)添加节流/防抖 - 监听解绑:组件销毁时移除监听器,避免泄漏
- 数据持久化:读频繁写稀疏场景使用
localStorage;大量结构化数据使用indexedDB
典型坑位:
- 在滚动事件中直接执行复杂计算或 DOM 操作 → 使用节流/防抖 +
requestAnimationFrame - 忘记在单页应用路由切换时解除事件监听 → 统一封装注册/解绑逻辑,或使用框架生命周期钩子
存储与缓存
localStorage:持久化键值,容量有限且字符串化sessionStorage:会话级存储indexedDB:结构化数据与事务支持- 缓存策略:合理的过期、二级缓存(内存 + 本地)、版本化升级
做本地存储时要考虑:
- 序列化格式:JSON 字符串是默认选择,必要时可考虑压缩或分段存储
- 失效策略:为每条数据附带时间戳与版本号,避免长期使用脏数据
- 容量上限:移动端浏览器容量较小,应避免在
localStorage中存放大体积二进制数据
网络与安全
fetch:现代拉取 API;支持流与AbortControllerXMLHttpRequest:传统异步请求接口- 跨域与 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 API:
pushState/replaceState与popstate事件实现前端路由 - URLSearchParams:解析与生成查询字符串
history.pushState({ page: 2 }, '', '?page=2');
window.addEventListener('popstate', e => {
// 读取 e.state
});
const params = new URLSearchParams(location.search);
params.get('page'); // '2'
Navigator 与权限
- Navigator:
navigator.userAgent、hardwareConcurrency、language等信息 - 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 最大优势是:
- 不需要手动计算元素位置与视口高度
- 在浏览器层面做节流与合并回调,减少滚动抖动与性能问题
常见场景:图片懒加载、列表曝光埋点、无限滚动分页等。
需要注意的是,不同浏览器实现对阈值与根元素支持略有差异,尤其在嵌套滚动容器中,需要通过 root 与 rootMargin 合理配置。
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);