JavaScript Web APIs 深度解析
JavaScript Web APIs 深度解析
Web APIs 是浏览器提供的一组接口,使 JavaScript 能够与浏览器和操作系统交互。
需要澄清一个常见误解:很多我们天天用的东西其实不属于 JavaScript 语言本身。`Array`、`Promise`、`Math` 属于 ECMAScript 语言规范;而 `document`、`fetch`、`localStorage`、`setTimeout` 都是浏览器额外注入的"超能力",统称 Web APIs。可以把 JavaScript 引擎想象成"大脑",Web APIs 就是浏览器给这个大脑装上的"手、眼、耳、记忆",让它能操作页面、发请求、存数据。
为什么要理解 Web APIs
现代前端框架(React、Vue)帮我们封装了大量 DOM 操作,但底层依然是 Web APIs。理解它们能帮你:
DOM API
DOM(Document Object Model,文档对象模型)把 HTML 文档解析成一棵可用 JavaScript 操作的节点树。
选择元素:
// 单个元素(返回第一个匹配,找不到返回 null)
const title = document.querySelector('.title');
const btn = document.querySelector('#submit');
// 多个元素(返回静态 NodeList)
const items = document.querySelectorAll('li.item');
items.forEach(item => console.log(item.textContent));
// 传统方法(性能略高,返回动态集合)
const byId = document.getElementById('app');
const byClass = document.getElementsByClassName('card');创建与修改元素:
// 创建
const div = document.createElement('div');
div.className = 'card';
div.textContent = 'Hello';
// 插入
document.body.appendChild(div);
document.body.insertBefore(div, document.body.firstChild);
// 现代插入 API
container.append(div, '文本节点'); // 可插入多个、支持字符串
container.prepend(div);
div.remove(); // 直接移除自身
// 属性操作
div.setAttribute('data-id', '123');
div.getAttribute('data-id');
div.removeAttribute('data-id');
console.log(div.dataset.id); // 通过 dataset 读取 data-*事件处理与事件委托:
事件委托是性能优化的关键技巧:利用事件冒泡,把子元素的监听器"上移"到父元素,一个监听器管理成百上千个子元素。
// 基础监听
button.addEventListener('click', (event) => {
console.log('被点击的元素:', event.target);
event.preventDefault(); // 阻止默认行为(如表单提交)
event.stopPropagation(); // 阻止冒泡
});
// 事件委托:一个监听器管理整个列表
document.querySelector('#todo-list').addEventListener('click', (e) => {
// 只有点到删除按钮才响应
if (e.target.matches('.delete-btn')) {
const item = e.target.closest('li');
item.remove();
}
});DOM 遍历与样式:
// 遍历
node.parentElement;
node.children; // 只含元素节点
node.nextElementSibling;
node.previousElementSibling;
// 样式操作
el.style.color = 'red';
el.classList.add('active');
el.classList.toggle('open');
el.classList.contains('active');
// 读取计算后的最终样式(含 CSS 文件里的)
const computed = getComputedStyle(el);
console.log(computed.fontSize);高性能 DOM 操作
DOM 操作慢,主要慢在"重排(reflow)"和"重绘(repaint)"。频繁增删会触发多次重排。用 DocumentFragment 可以把多次操作合并为一次。
// 反例:循环里 1000 次 appendChild,触发大量重排
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
list.appendChild(li); // 每次都可能重排
}
// 正例:先在内存里的 fragment 组装,一次性插入
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
list.appendChild(fragment); // 只触发一次重排实测中,插入 1 万个节点时,DocumentFragment 方案通常比逐个 appendChild 快 3~10 倍。
Fetch API
Fetch 是基于 Promise 的现代网络请求接口,取代了古老的 XMLHttpRequest。
// GET 请求
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
// 关键坑:fetch 只在网络故障时 reject,
// HTTP 4xx/5xx 不会抛错,必须手动检查 res.ok
if (!res.ok) {
throw new Error(`HTTP 错误:${res.status}`);
}
return res.json();
}
// POST 请求
async function createUser(data) {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
credentials: 'include', // 携带 cookie
});
return res.json();
}请求超时与取消(AbortController):
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, { signal: controller.signal });
return await res.json();
} catch (err) {
if (err.name === 'AbortError') {
throw new Error('请求超时');
}
throw err;
} finally {
clearTimeout(timer);
}
}Fetch 请求方法与语义:
| 方法 | 语义 | 幂等 |
| --- | --- | --- |
| GET | 获取资源 | 是 |
| POST | 新建资源 | 否 |
| PUT | 整体替换资源 | 是 |
| PATCH | 部分更新资源 | 否 |
| DELETE | 删除资源 | 是 |
存储 API
浏览器提供多种客户端存储方案,选型要看数据大小、生命周期、是否需要索引查询。
// localStorage:持久存储,关闭浏览器也不丢
localStorage.setItem('theme', 'dark');
localStorage.getItem('theme'); // 'dark'
localStorage.removeItem('theme');
// 存对象要序列化
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alice' }));
const user = JSON.parse(localStorage.getItem('user'));
// sessionStorage:仅当前标签页会话有效,关标签页即清除
sessionStorage.setItem('draft', '临时草稿');IndexedDB: 面向大量结构化数据的异步数据库,支持索引和事务。
const request = indexedDB.open('MyDB', 1);
request.onupgradeneeded = (e) => {
const db = e.target.result;
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('byName', 'name', { unique: false });
};
request.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction('users', 'readwrite');
tx.objectStore('users').put({ id: 1, name: 'Alice' });
};存储方案对比:
| 方案 | 容量 | 生命周期 | 数据类型 | 同步/异步 | 随请求发送 |
| --- | --- | --- | --- | --- | --- |
| Cookie | 约 4KB | 可设过期 | 字符串 | 同步 | 是(每次请求) |
| localStorage | 约 5-10MB | 永久 | 字符串 | 同步 | 否 |
| sessionStorage | 约 5-10MB | 标签页会话 | 字符串 | 同步 | 否 |
| IndexedDB | 数百 MB 以上 | 永久 | 结构化对象 | 异步 | 否 |
选型经验:小配置用 localStorage;临时状态用 sessionStorage;需要随请求带给服务端的凭证用 Cookie;大量离线数据用 IndexedDB。
其他常用 Web APIs
地理位置 API:
navigator.geolocation.getCurrentPosition(
(pos) => {
console.log('纬度:', pos.coords.latitude);
console.log('经度:', pos.coords.longitude);
},
(err) => console.error('定位失败:', err.message),
{ enableHighAccuracy: true, timeout: 5000 }
);通知 API:
async function notify(title, body) {
if (Notification.permission !== 'granted') {
const perm = await Notification.requestPermission();
if (perm !== 'granted') return;
}
new Notification(title, { body, icon: '/icon.png' });
}IntersectionObserver(图片懒加载利器):
比监听 scroll 事件性能高得多,是实现懒加载、无限滚动的现代标准做法。
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 进入视口才加载真图
observer.unobserve(img);
}
});
}, { rootMargin: '100px' });
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));Web Workers(后台线程):
JavaScript 主线程是单线程的,长时间计算会卡死界面。Web Worker 把重活丢到后台线程。
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ numbers: [1, 2, 3, 4, 5] });
worker.onmessage = (e) => {
console.log('计算结果:', e.data);
};
// worker.js
self.onmessage = (e) => {
const sum = e.data.numbers.reduce((a, b) => a + b, 0);
self.postMessage(sum); // 算完发回主线程
};Service Workers(离线缓存与 PWA 核心):
Service Worker 是一个运行在浏览器后台的网络代理,可拦截请求、提供离线缓存、推送通知。
// 注册
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
// sw.js:缓存优先策略
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
});WebSocket(实时双向通信):
const ws = new WebSocket('wss://example.com/chat');
ws.onopen = () => ws.send(JSON.stringify({ type: 'join', room: 'lobby' }));
ws.onmessage = (e) => console.log('收到消息:', e.data);
ws.onclose = () => console.log('连接关闭');真实案例:图片密集页面的加载优化
某新闻站首页有 60+ 张图片,首屏加载耗时 4.2 秒。优化方案:
优化后首屏可交互时间从 4.2 秒降到 1.3 秒,首屏图片请求数从 60 降到 6,流量节省约 70%。
通信 API 对比
| 方案 | 方向 | 实时性 | 适用场景 |
| --- | --- | --- | --- |
| Fetch / XHR | 客户端发起 | 一次性 | 常规数据请求 |
| SSE (EventSource) | 服务端 → 客户端 | 高 | 消息推送、股价 |
| WebSocket | 双向 | 极高 | 聊天、游戏、协同 |
| 轮询 | 客户端发起 | 低 | 兼容性兜底 |
Server-Sent Events(SSE)
SSE 是"服务端单向推送"的轻量方案,基于普通 HTTP,比 WebSocket 简单得多,自带断线重连,适合消息通知、进度、股价、AI 流式输出等场景。
// 客户端
const es = new EventSource('/api/stream');
es.onmessage = (e) => console.log('默认事件:', e.data);
es.addEventListener('price', (e) => {
const data = JSON.parse(e.data);
updatePrice(data);
});
es.onerror = () => console.log('连接中断,浏览器会自动重连');
// 用完记得关闭,否则会一直占用连接
// es.close();
// 服务端(Express)——注意特定的响应头和数据格式
app.get('/api/stream', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const timer = setInterval(() => {
// 每条消息以 data: 开头,以两个换行结束
res.write(`event: price\ndata: ${JSON.stringify({ v: Math.random() })}\n\n`);
}, 1000);
req.on('close', () => clearInterval(timer)); // 客户端断开要清理
});SSE vs WebSocket 抉择: 只需服务端推、数据是文本、想省心用 SSE;需要双向实时、二进制、低延迟(游戏、协同编辑)用 WebSocket。
高级 Fetch:流式读取与进度
Fetch 的 `response.body` 是一个 ReadableStream,可以边下载边处理,实现大文件进度条或 AI 逐字输出效果。
async function downloadWithProgress(url, onProgress) {
const res = await fetch(url);
const total = Number(res.headers.get('Content-Length')) || 0;
const reader = res.body.getReader();
let received = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
if (total) onProgress(received / total); // 0 到 1 的进度
}
return new Blob(chunks);
}
await downloadWithProgress('/big-file.zip', p => {
console.log(`已下载 ${(p * 100).toFixed(1)}%`);
});读取流式文本(如 AI 接口的 SSE/流式响应):
const res = await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ q }) });
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
appendToUI(value); // 逐块把文本追加到界面,实现打字机效果
}请求缓存与去重
同一份数据被多个组件同时请求时,应做请求去重,避免重复网络往返。
const inflight = new Map(); // 正在进行的请求缓存
function dedupedFetch(url) {
if (inflight.has(url)) return inflight.get(url); // 复用进行中的 Promise
const promise = fetch(url)
.then(r => r.json())
.finally(() => inflight.delete(url)); // 完成后清掉
inflight.set(url, promise);
return promise;
}
// 同一时刻发起 3 次,实际只有 1 个网络请求
const [a, b, c] = await Promise.all([
dedupedFetch('/api/config'),
dedupedFetch('/api/config'),
dedupedFetch('/api/config'),
]);MutationObserver:监听 DOM 变化
当你无法控制某段 DOM 何时被第三方脚本修改时,MutationObserver 可以异步监听 DOM 树变化,比早已废弃的 Mutation Events 性能好得多。
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'childList') {
console.log('新增节点:', m.addedNodes.length);
} else if (m.type === 'attributes') {
console.log(`属性 ${m.attributeName} 变了`);
}
}
});
observer.observe(document.querySelector('#app'), {
childList: true, // 子节点增删
attributes: true, // 属性变化
subtree: true, // 包括所有后代
});
// 停止监听
// observer.disconnect();ResizeObserver:监听元素尺寸
监听元素自身尺寸变化,比监听 window 的 resize 更精准(能感知非窗口引起的尺寸变化,如内容撑开、flex 重排),是响应式组件的利器。
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(`尺寸变为 ${width} × ${height}`);
// 例如根据宽度切换布局
entry.target.classList.toggle('compact', width < 400);
}
});
ro.observe(document.querySelector('.chart-container'));三大 Observer 对比
| API | 监听什么 | 典型用途 |
| --- | --- | --- |
| IntersectionObserver | 元素与视口/祖先的交叉状态 | 懒加载、无限滚动、曝光埋点 |
| MutationObserver | DOM 树结构与属性变化 | 监控第三方注入、富文本编辑器 |
| ResizeObserver | 元素尺寸变化 | 响应式组件、图表自适应 |
History API 与前端路由
单页应用(SPA)的前端路由本质就是用 History API 在不刷新页面的情况下改变 URL。
// 压入新历史记录,URL 变化但页面不刷新
history.pushState({ page: 'about' }, '', '/about');
// 替换当前记录(不新增历史条目)
history.replaceState({ page: 'home' }, '', '/home');
// 监听前进/后退按钮
window.addEventListener('popstate', (e) => {
console.log('导航到:', location.pathname, '状态:', e.state);
renderRoute(location.pathname);
});
// 一个极简路由器
function navigate(path) {
history.pushState({}, '', path);
renderRoute(path);
}
document.addEventListener('click', (e) => {
const link = e.target.closest('a[data-spa]');
if (link) {
e.preventDefault();
navigate(link.getAttribute('href'));
}
});剪贴板、分享与文件
// 现代剪贴板 API(需 HTTPS + 用户手势)
await navigator.clipboard.writeText('要复制的内容');
const text = await navigator.clipboard.readText();
// 原生分享(移动端调起系统分享面板)
if (navigator.share) {
await navigator.share({ title: '标题', text: '描述', url: location.href });
}
// File API:读取用户选择的文件
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
console.log(file.name, file.size, file.type);
const text = await file.text(); // 读文本
const buf = await file.arrayBuffer(); // 读二进制
const url = URL.createObjectURL(file); // 生成预览 URL
img.src = url;
// 用完释放,避免内存泄漏
// URL.revokeObjectURL(url);
});Web Worker 进阶:结构化克隆与 Transferable
主线程与 Worker 之间 postMessage 传数据默认走结构化克隆(深拷贝),大数据会有拷贝开销。对 ArrayBuffer 等可转移对象,用 Transferable 做零拷贝转移,性能天差地别。
// 普通传递:大 buffer 会被完整拷贝一份
worker.postMessage({ buffer });
// 零拷贝转移:所有权移交 Worker,主线程随后不能再用该 buffer
const buffer = new ArrayBuffer(64 * 1024 * 1024); // 64MB
worker.postMessage({ buffer }, [buffer]); // 第二个参数声明转移列表
// 转移后 buffer.byteLength === 0,转移 64MB 从数十毫秒降到接近 0结构化克隆的限制: 函数、DOM 节点、Symbol 无法克隆,会抛错;但 Date、RegExp、Map、Set、ArrayBuffer、Blob 等可以。可用 `structuredClone()` 在主线程做深拷贝。
性能与用户体验相关 API
// requestIdleCallback:在浏览器空闲时执行低优先级任务,不抢占关键渲染
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length) {
processTask(tasks.shift());
}
});
// Page Visibility:标签页切到后台时暂停轮询/动画,省电省流量
document.addEventListener('visibilitychange', () => {
if (document.hidden) stopPolling();
else startPolling();
});
// navigator.sendBeacon:页面卸载时可靠地发送埋点,不会被打断
window.addEventListener('pagehide', () => {
navigator.sendBeacon('/analytics', JSON.stringify({ event: 'leave' }));
});Canvas 与 OffscreenCanvas
Canvas 提供像素级绘图能力,是图表、游戏、图像处理的基础。OffscreenCanvas 更可把绘制搬到 Worker,彻底不阻塞主线程。
const canvas = document.querySelector('#chart');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#4f46e5';
ctx.fillRect(10, 10, 100, 50);
ctx.beginPath();
ctx.arc(200, 100, 40, 0, Math.PI * 2);
ctx.stroke();
// OffscreenCanvas:把渲染移交 Worker,主线程零负担
const offscreen = document.querySelector('#big').transferControlToOffscreen();
const worker = new Worker('render-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]); // 转移所有权
// render-worker.js
self.onmessage = (e) => {
const ctx = e.data.canvas.getContext('2d');
function draw() {
ctx.clearRect(0, 0, 800, 600);
// ...重绘逻辑,即使很耗时也不卡主线程
requestAnimationFrame(draw);
}
draw();
};BroadcastChannel:跨标签页通信
同源的多个标签页之间广播消息,常用于"一个页面登出,其他页面同步登出"这类场景。
const channel = new BroadcastChannel('auth');
// A 标签页登出后广播
channel.postMessage({ type: 'logout' });
// 其他标签页监听并响应
channel.onmessage = (e) => {
if (e.data.type === 'logout') {
location.href = '/login';
}
};跨标签页通信的其他方案还有 `storage` 事件(localStorage 变化会通知其他同源标签页)和 SharedWorker(多标签共享一个 Worker 实例)。
拖放 API(Drag and Drop)
// 可拖动元素
dragItem.draggable = true;
dragItem.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', dragItem.id);
e.dataTransfer.effectAllowed = 'move';
});
// 放置区域
dropZone.addEventListener('dragover', (e) => {
e.preventDefault(); // 必须阻止默认行为,否则无法触发 drop
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
const id = e.dataTransfer.getData('text/plain');
dropZone.appendChild(document.getElementById(id));
});
// 拖拽上传文件
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
const files = [...e.dataTransfer.files]; // 用户从桌面拖入的文件
files.forEach(uploadFile);
});网络与设备信息 API
// Network Information:根据网速调整策略(如慢网络不自动播放视频)
const conn = navigator.connection;
if (conn) {
console.log('网络类型:', conn.effectiveType); // '4g' | '3g' | '2g'
console.log('省流量模式:', conn.saveData);
if (conn.effectiveType === '2g' || conn.saveData) {
loadLowQualityAssets(); // 弱网降级
}
}
// 剪贴板粘贴图片、屏幕方向、震动等设备能力
navigator.vibrate?.([200, 100, 200]); // 移动端震动反馈常见坑
最佳实践
性能优化:
兼容性:
安全性:
代码组织:
总结
| 类别 | 代表 API | 核心用途 |
| --- | --- | --- |
| DOM | querySelector、classList、DocumentFragment | 操作页面结构与样式 |
| 网络 | Fetch、AbortController、WebSocket | 数据请求与实时通信 |
| 存储 | localStorage、sessionStorage、IndexedDB、Cookie | 客户端数据持久化 |
| 观察者 | IntersectionObserver、MutationObserver | 高性能监听变化 |
| 并发/离线 | Web Worker、Service Worker | 后台计算与离线能力 |
| 设备 | Geolocation、Notification、MediaDevices | 访问系统能力 |
一句话:框架帮你隐藏了 Web APIs,但真正遇到性能瓶颈和疑难杂症时,能救你的正是对底层 Web APIs 的理解。
实际应用场景
前端应用: 表单处理、数据可视化、实时通信、离线功能。
交互体验: 拖拽、键盘导航、触摸事件、动画。
工具开发: 浏览器扩展、开发者工具、性能分析工具。
PWA(渐进式 Web 应用): 离线访问、安装到主屏幕、推送通知、后台同步。