浏览器篇 | Storage
前言
前端开发离不开”存东西”。用户的登录态要存、主题偏好要存、购物车数据要存、表单草稿也想存……但浏览器提供的存储方案不止一种,而且每种方案的特性差异很大。面试中经常会遇到这样的问题:
- Cookie 和 localStorage 有什么区别?
- sessionStorage 关掉标签页就没了,那刷新呢?
- SameSite 属性到底在防什么?
- 什么时候该用 IndexedDB?
很多人对这些问题只有一个模糊的印象——“Cookie 小、localStorage 大”,但细节一追就含糊了。
本章我们就把浏览器端的几种主流存储方案——Cookie、localStorage、sessionStorage、IndexedDB——逐个拆解,从属性、限制、适用场景、安全性等维度建立一个清晰的对比框架。读完之后,面试中再遇到存储相关的问题,你应该能做到条理清楚、细节到位。
诊断自测
在开始正文之前,先用几道题测测你目前对浏览器存储的理解。答不上来也没关系,读完全文再回来对照。
Q1:Cookie 的 HttpOnly 属性有什么作用?设置了 HttpOnly 的 Cookie,前端 JS 还能读取到吗?
点击查看答案
设置了 HttpOnly 的 Cookie,前端 JavaScript 无法通过 document.cookie 读取或修改。它只会在浏览器发起 HTTP 请求时自动附带在请求头中。这是防御 XSS 攻击窃取 Cookie 的重要手段——即使页面被注入了恶意脚本,也拿不到标记了 HttpOnly 的 Cookie(比如存放 session ID 的那个)。
Q2:localStorage 和 sessionStorage 的数据,在同一个浏览器的不同标签页之间能共享吗?
点击查看答案
localStorage 在同源的所有标签页之间共享,只要协议 + 域名 + 端口一致,不同标签页读取到的 localStorage 数据是同一份。sessionStorage 则不共享——每个标签页都有自己独立的 sessionStorage,即使是同一个 URL。但有一个例外:通过 window.open 或 <a target="_blank"> 打开的新标签页,会复制一份原标签页的 sessionStorage(注意是复制,之后各自独立)。
Q3:Cookie 的 SameSite=Lax 和 SameSite=Strict 有什么区别?
点击查看答案
Strict:完全禁止第三方网站携带该 Cookie。如果用户在 A 站点击链接跳到 B 站,B 站的 Cookie 不会被带上,用户会发现自己”没登录”。Lax:允许部分安全的跨站请求(如顶级导航的 GET 请求)携带 Cookie,但阻止 POST 表单、iframe、AJAX 等方式的跨站携带。Lax 是现代浏览器的默认值,在安全性和用户体验之间取得了平衡。
一、Cookie:最古老也最复杂的存储方案
Cookie 是浏览器最早支持的存储机制,诞生于 1994 年。它最初的设计目的是让无状态的 HTTP 协议能够记住用户信息——服务器通过响应头 Set-Cookie 告诉浏览器”记住这个值”,浏览器在后续请求中通过 Cookie 请求头自动把它带回来。
1.1 Cookie 的核心属性
一个完整的 Cookie 由名值对 + 一组属性组成。面试中经常会让你列举并解释这些属性:
Set-Cookie: sessionId=abc123; Domain=.example.com; Path=/; Expires=Thu, 01 Jan 2026 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax
Domain
- 指定 Cookie 对哪些域名有效
- 如果设置为
.example.com,那么www.example.com、api.example.com等子域都能拿到这个 Cookie - 如果不设置,默认是当前页面的域名(不含子域)
- 注意:不能设置为与当前域完全无关的域名,比如
a.com的页面不能给b.com设置 Cookie
Path
- 限制 Cookie 只在指定路径及其子路径下有效
- 比如
Path=/admin,那么/admin/dashboard可以拿到,但/home拿不到 - 默认值是设置 Cookie 时的页面路径
Expires / Max-Age
Expires:指定一个绝对的过期时间(UTC 格式)Max-Age:指定 Cookie 的有效时长(单位:秒),优先级高于Expires- 如果两个都不设置,Cookie 就是会话级别的——浏览器关闭后就会被清除(和 sessionStorage 类似)
- 设置
Max-Age=0或者过去的Expires可以立即删除一个 Cookie
HttpOnly
- 设置后,JavaScript 无法通过
document.cookie读取或修改这个 Cookie - 它只会在 HTTP 请求中自动附带
- 防御 XSS 的关键属性:即使攻击者注入了恶意脚本,也拿不到标记了 HttpOnly 的敏感 Cookie
Secure
- 设置后,Cookie 只在 HTTPS 连接中才会被发送
- 在 HTTP 页面中,浏览器不会发送带有 Secure 标记的 Cookie
- 现代最佳实践:所有敏感 Cookie 都应该加上 Secure
SameSite
这是近年来最重要的 Cookie 属性,直接关系到 CSRF 防御:
Strict:完全阻止跨站请求携带 Cookie。最安全,但用户体验较差(从外站跳转过来会丢失登录态)Lax(现代浏览器默认值):允许顶级导航的 GET 请求携带 Cookie,阻止 POST / iframe / AJAX 等跨站携带None:完全不限制跨站携带,但必须同时设置Secure,否则会被浏览器拒绝
// SameSite 的典型设置
Set-Cookie: token=xxx; SameSite=Lax; Secure; HttpOnly
1.2 Cookie 的限制
- 大小限制:单个 Cookie 最大约 4KB(包括名称和值)
- 数量限制:每个域名下通常限制 50 个左右的 Cookie(不同浏览器略有差异)
- 性能影响:Cookie 会随每个 HTTP 请求自动发送到服务器。如果存了太多 Cookie,每个请求都会带上这些额外数据,增加网络开销
- 操作不便:
document.cookie的 API 设计非常原始,读取时返回一个拼接的字符串,写入时是追加而不是覆盖
// 读取:返回 "name1=value1; name2=value2" 这样的字符串
console.log(document.cookie);
// 写入:一次只能设一个
document.cookie = "username=Alice; Max-Age=3600; Path=/";
// 删除:把 Max-Age 设为 0
document.cookie = "username=; Max-Age=0; Path=/";
1.3 Cookie 的适用场景
- 身份认证:session ID、JWT token(配合 HttpOnly + Secure + SameSite)
- 用户偏好:语言设置、主题选择(如果需要服务端读取的话)
- 追踪与分析:第三方 Cookie 用于广告追踪(正在被各大浏览器逐步限制)
二、Web Storage:localStorage 与 sessionStorage
HTML5 引入了 Web Storage API,提供了比 Cookie 更简洁、容量更大的客户端存储方案。它包含两个兄弟:localStorage 和 sessionStorage。
2.1 API 一览
两者的 API 完全一致,非常简洁:
// 写入
localStorage.setItem('theme', 'dark');
// 读取
const theme = localStorage.getItem('theme'); // 'dark'
// 删除单个
localStorage.removeItem('theme');
// 清空所有
localStorage.clear();
// 获取长度
console.log(localStorage.length);
// 按索引获取 key
console.log(localStorage.key(0));
注意:Web Storage 只能存储字符串。 如果你想存对象或数组,需要手动序列化:
// 存对象
localStorage.setItem('user', JSON.stringify({ name: 'Alice', age: 25 }));
// 取对象
const user = JSON.parse(localStorage.getItem('user'));
这里有个经典的坑:如果 getItem 返回 null(key 不存在),JSON.parse(null) 的结果是 null,不会报错。但如果存的是一个非法 JSON 字符串,JSON.parse 就会抛异常。所以在取数据时最好加上 try-catch。
2.2 localStorage vs sessionStorage:关键差异
| 特性 | localStorage | sessionStorage |
|---|---|---|
| 生命周期 | 永久,除非手动删除或清除浏览器数据 | 会话级别,标签页关闭后清除 |
| 跨标签页共享 | 同源下共享 | 不共享(每个标签页独立) |
| 存储大小 | 约 5-10MB(因浏览器而异) | 约 5-10MB |
| 随请求发送 | 不会 | 不会 |
| 刷新页面 | 数据保留 | 数据保留 |
几个容易混淆的点:
- sessionStorage 刷新页面不会丢失——很多人以为”会话级别”意味着刷新就没了,其实不是。只有关闭标签页才会清除。
- sessionStorage 通过链接打开新标签页时会复制——通过
window.open或<a target="_blank">打开的新标签页,会拿到原标签页 sessionStorage 的一份副本,之后各自独立修改互不影响。 - localStorage 有
storage事件——当一个标签页修改了 localStorage,同源的其他标签页会收到storage事件,可以用来做简单的跨标签页通信。
// 标签页 A 写入
localStorage.setItem('message', 'hello from A');
// 标签页 B 监听
window.addEventListener('storage', (e) => {
console.log('key:', e.key); // 'message'
console.log('oldValue:', e.oldValue); // null 或之前的值
console.log('newValue:', e.newValue); // 'hello from A'
console.log('url:', e.url); // 触发变更的页面 URL
});
注意:storage 事件只在其他标签页触发,当前标签页不会收到自己的修改事件。
2.3 Web Storage 的局限
- 只能存字符串:需要手动 JSON 序列化/反序列化,不支持二进制数据
- 同步 API:所有操作都是同步的,大量数据读写可能阻塞主线程
- 无过期机制:localStorage 没有内置的过期时间,需要自己实现
// 一个简单的带过期时间的 localStorage 封装
const storage = {
set(key, value, ttl) {
const item = {
value,
expire: ttl ? Date.now() + ttl : null
};
localStorage.setItem(key, JSON.stringify(item));
},
get(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
const item = JSON.parse(raw);
if (item.expire && Date.now() > item.expire) {
localStorage.removeItem(key);
return null;
}
return item.value;
}
};
三、IndexedDB:浏览器中的”数据库”
Cookie 和 Web Storage 都只适合存储少量的简单数据。当你需要在浏览器端存储大量结构化数据——比如离线缓存的文章列表、聊天记录、图片资源——就需要 IndexedDB。
3.1 IndexedDB 的核心特性
- 大容量:理论上没有硬性上限,通常可以使用磁盘空间的很大一部分(Chrome 默认可用空间约为磁盘的 80%,每个源至少 10GB)
- 异步 API:所有操作都是异步的,不会阻塞主线程
- 支持事务:读写操作在事务中完成,保证数据一致性
- 存储类型丰富:可以存 JavaScript 对象、二进制数据(Blob、ArrayBuffer)、文件等
- 支持索引:可以对对象的属性建立索引,支持范围查询
- 同源限制:和 Web Storage 一样,受同源策略约束
3.2 基本使用
IndexedDB 的原生 API 比较繁琐,是基于事件回调的风格:
// 1. 打开(或创建)数据库
const request = indexedDB.open('myApp', 1);
// 2. 数据库版本升级时创建对象仓库(表)
request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('nameIndex', 'name', { unique: false });
}
};
// 3. 打开成功后进行读写
request.onsuccess = (e) => {
const db = e.target.result;
// 写入
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.put({ id: 1, name: 'Alice', age: 25 });
store.put({ id: 2, name: 'Bob', age: 30 });
// 读取
const getTx = db.transaction('users', 'readonly');
const getStore = getTx.objectStore('users');
const getReq = getStore.get(1);
getReq.onsuccess = () => {
console.log(getReq.result); // { id: 1, name: 'Alice', age: 25 }
};
};
在实际项目中,我们通常会用封装好的库来操作 IndexedDB,比如 Dexie.js 或 idb(由 Jake Archibald 开发的 Promise 封装):
// 使用 idb 库(Promise 风格)
import { openDB } from 'idb';
const db = await openDB('myApp', 1, {
upgrade(db) {
db.createObjectStore('users', { keyPath: 'id' });
}
});
// 写入
await db.put('users', { id: 1, name: 'Alice' });
// 读取
const user = await db.get('users', 1);
3.3 IndexedDB 的适用场景
- 离线应用:PWA 中缓存大量数据,让应用在断网时仍可使用
- 大数据集:需要在客户端存储和查询大量记录(比如邮件客户端、笔记应用)
- 二进制文件:存储图片、音频等 Blob 数据
- 复杂查询:需要按索引范围检索数据的场景
四、存储方案对比与选型
这是面试中的经典总结题。把所有方案放在一起对比:
| 特性 | Cookie | localStorage | sessionStorage | IndexedDB |
|---|---|---|---|---|
| 容量 | ~4KB | ~5-10MB | ~5-10MB | 数百 MB 以上 |
| 生命周期 | 可设过期时间 | 永久 | 标签页关闭清除 | 永久 |
| 随请求发送 | 是 | 否 | 否 | 否 |
| API 风格 | 原始字符串 | 同步、简洁 | 同步、简洁 | 异步、事务 |
| 数据类型 | 字符串 | 字符串 | 字符串 | 结构化数据、二进制 |
| 跨标签页共享 | 是(同域) | 是(同源) | 否 | 是(同源) |
| 可被 JS 访问 | 看 HttpOnly | 是 | 是 | 是 |
| 服务端可读取 | 是 | 否 | 否 | 否 |
选型建议
需要服务端读取?→ Cookie
比如身份认证的 token、session ID。Cookie 的独特之处在于它会自动随请求发送给服务器,这是其他方案做不到的。
简单的客户端状态?→ localStorage / sessionStorage
比如用户偏好(主题、语言)、表单草稿、页面状态。数据量小、结构简单、不需要服务端参与。需要持久化就用 localStorage,只在当前会话有效就用 sessionStorage。
大量数据 / 离线场景?→ IndexedDB
数据量大、需要查询、需要存二进制、或者在 PWA 中做离线支持,IndexedDB 是唯一合适的选择。
需要注意的安全原则:
- 不要在 localStorage/sessionStorage 中存敏感信息(token、密码等),因为 XSS 攻击可以轻松读取
- 敏感信息如果必须存在客户端,放 Cookie 里并设置
HttpOnly + Secure + SameSite - IndexedDB 中的数据同样可以被 JS 访问,也不适合存敏感信息
五、补充:几个常见面试追问
5.1 Cookie 和 Session 的关系
这是一个常见的混淆点。Cookie 是浏览器端的存储机制,Session 是服务端的会话机制。 它们的关系是:Session 通常依赖 Cookie 来传递 session ID——服务器在 Set-Cookie 里把 session ID 发给浏览器,浏览器后续请求通过 Cookie 头把它带回来,服务器根据这个 ID 找到对应的 Session 数据。
但 Session 不是必须依赖 Cookie——也可以通过 URL 参数、自定义请求头等方式传递 session ID。只是 Cookie 最方便,所以成了主流方案。
5.2 第三方 Cookie 的消亡
第三方 Cookie(由非当前页面域名设置的 Cookie)长期以来被用于跨站追踪(比如广告平台追踪你在不同网站上的浏览行为)。由于隐私问题,各大浏览器正在逐步限制或淘汰第三方 Cookie:
- Safari 和 Firefox 已经默认阻止第三方 Cookie
- Chrome 在推进 Privacy Sandbox 方案作为替代
5.3 Storage 配额查询
现代浏览器提供了 Storage API 来查询存储配额:
if (navigator.storage && navigator.storage.estimate) {
const estimate = await navigator.storage.estimate();
console.log(`已用: ${estimate.usage} bytes`);
console.log(`配额: ${estimate.quota} bytes`);
console.log(`使用率: ${(estimate.usage / estimate.quota * 100).toFixed(2)}%`);
}
常见误区
误区一:“localStorage 是永久的,所以数据绝对不会丢”
虽然 localStorage 不会自动过期,但它可以被清除:用户手动清除浏览器数据、浏览器在存储空间不足时可能会清理(尤其是移动端 Safari 的”7 天限制”策略)、隐私模式下关闭窗口后数据也会被清除。所以不要把 localStorage 当作可靠的持久化方案,重要数据还是应该同步到服务端。
误区二:“Cookie 的大小限制是 4KB,所以能存很多条”
4KB 是单个 Cookie 的大小限制(包含名称、值和属性),而且每个域名下的 Cookie 总数也有限制(通常约 50 个)。更关键的是,所有 Cookie 都会随请求发送,几十个 Cookie 加起来可能有几 KB 的额外开销——每个请求都要带上这些数据,这对性能的影响不可忽视。
误区三:“sessionStorage 和 Session 是一回事”
sessionStorage 是浏览器端的存储 API,数据存在客户端;Session 是服务端的会话机制,数据存在服务器。两者名字相似,但概念完全不同。sessionStorage 的”session”指的是浏览器标签页的生命周期,而 Session 的”session”指的是用户和服务器之间的会话。
误区四:“IndexedDB 太复杂了,不如都用 localStorage”
IndexedDB 的原生 API 确实繁琐,但使用 idb 或 Dexie.js 这类封装库后,体验和 localStorage 差不多。而且 IndexedDB 的异步特性意味着它不会阻塞主线程,在存储大量数据时反而比 localStorage 的同步读写更不容易卡顿。选型应该基于实际需求(数据量、数据类型、是否需要查询),而不是 API 的复杂度。
小结
本章我们从浏览器最古老的 Cookie 讲到现代的 IndexedDB,完整梳理了浏览器端的存储方案。
核心要点
- Cookie 是唯一会随 HTTP 请求自动发送的存储方案,适合身份认证,但容量小(4KB)、API 原始
- Cookie 的安全属性(HttpOnly、Secure、SameSite)是前端安全的重要防线
- localStorage 持久化、同源共享、约 5-10MB,适合简单的客户端状态
- sessionStorage 标签页级别、关闭即清、刷新保留,适合临时状态
- IndexedDB 容量大、异步、支持结构化数据和索引,适合离线应用和大数据集
- 不要在 localStorage 中存敏感信息,XSS 可以轻松读取
- 选型核心逻辑:需要服务端读取 → Cookie;简单客户端状态 → Web Storage;大量数据 → IndexedDB
本章思维导图
- Cookie
- 核心属性
- Domain / Path:作用范围
- Expires / Max-Age:生命周期
- HttpOnly:禁止 JS 访问
- Secure:仅 HTTPS
- SameSite:跨站策略(Strict / Lax / None)
- 限制:4KB / 约 50 个 / 随请求发送
- 场景:身份认证、服务端需要读取的状态
- 核心属性
- Web Storage
- localStorage
- 永久存储、同源共享
- 支持 storage 事件(跨标签页通信)
- sessionStorage
- 标签页级别、关闭清除、刷新保留
- 新标签页打开时复制(独立副本)
- 共同特点
- ~5-10MB、只能存字符串、同步 API
- 不随请求发送
- localStorage
- IndexedDB
- 大容量、异步、事务
- 支持对象、二进制、索引查询
- 适合离线应用、大数据集
- 选型决策
- 需要服务端读取 → Cookie
- 简单客户端状态 → Web Storage
- 大量/结构化数据 → IndexedDB
- 安全原则
- 敏感信息不放 Web Storage
- Cookie 加 HttpOnly + Secure + SameSite
练习挑战
第一题 ⭐(基础):判断对错
下面四个说法,哪些是正确的?
localStorage的数据在浏览器关闭后会被清除sessionStorage在页面刷新后数据仍然保留- 设置了
HttpOnly的 Cookie 无法通过document.cookie读取 Cookie的SameSite默认值是None
点击查看答案
- ❌ 第 1 条错误:
localStorage是永久存储,浏览器关闭后数据仍然保留,除非手动删除 - ✅ 第 2 条正确:
sessionStorage在刷新页面后数据保留,只有关闭标签页才会清除 - ✅ 第 3 条正确:
HttpOnly的作用就是阻止 JavaScript 访问 - ❌ 第 4 条错误:现代浏览器(Chrome 80+)的
SameSite默认值是Lax,不是None
第二题 ⭐⭐(进阶):实现一个带过期时间的 localStorage 封装
要求:
set(key, value, ttl)—— ttl 为过期时间(毫秒),可选get(key)—— 如果已过期则返回 null 并自动清除- 存取对象时不需要调用方手动
JSON.stringify/JSON.parse
点击查看答案
class ExpiringStorage {
set(key, value, ttl) {
const item = {
value,
expire: ttl ? Date.now() + ttl : null
};
try {
localStorage.setItem(key, JSON.stringify(item));
} catch (e) {
console.warn('localStorage 写入失败:', e);
}
}
get(key) {
const raw = localStorage.getItem(key);
if (raw === null) return null;
try {
const item = JSON.parse(raw);
// 兼容不是本封装写入的数据
if (!item || typeof item !== 'object' || !('value' in item)) {
return item;
}
if (item.expire && Date.now() > item.expire) {
localStorage.removeItem(key);
return null;
}
return item.value;
} catch {
return raw; // 如果 parse 失败,返回原始字符串
}
}
remove(key) {
localStorage.removeItem(key);
}
}
// 使用
const storage = new ExpiringStorage();
storage.set('token', 'abc123', 60 * 1000); // 1 分钟过期
storage.set('theme', 'dark'); // 永不过期
console.log(storage.get('token')); // 'abc123'(1 分钟内)
console.log(storage.get('theme')); // 'dark'
关键点:将值和过期时间一起序列化后存入 localStorage,取出时先检查是否过期。注意 try-catch 处理 JSON 解析异常和存储空间满的情况。
第三题 ⭐⭐⭐(综合):利用 localStorage 的 storage 事件实现简单的跨标签页消息总线
要求:
- 实现
send(channel, data)和on(channel, callback)方法 - 消息只在其他标签页被接收(和 BroadcastChannel API 类似)
- 发送后自动清理 localStorage 中的临时数据
点击查看答案
class CrossTabBus {
constructor(prefix = '__cross_tab_bus__') {
this.prefix = prefix;
this.listeners = {};
window.addEventListener('storage', (e) => {
if (!e.key || !e.key.startsWith(this.prefix)) return;
if (e.newValue === null) return; // 删除事件,忽略
const channel = e.key.slice(this.prefix.length);
const callbacks = this.listeners[channel];
if (!callbacks || callbacks.length === 0) return;
try {
const data = JSON.parse(e.newValue);
callbacks.forEach(cb => cb(data.payload));
} catch {
// 忽略解析失败
}
});
}
send(channel, data) {
const key = this.prefix + channel;
const message = JSON.stringify({
payload: data,
timestamp: Date.now()
});
localStorage.setItem(key, message);
// 发送后立即清理,避免污染 localStorage
// 用 setTimeout 确保 storage 事件已经触发
setTimeout(() => localStorage.removeItem(key), 100);
}
on(channel, callback) {
if (!this.listeners[channel]) {
this.listeners[channel] = [];
}
this.listeners[channel].push(callback);
// 返回取消订阅函数
return () => {
this.listeners[channel] = this.listeners[channel].filter(cb => cb !== callback);
};
}
}
// 使用
const bus = new CrossTabBus();
// 标签页 A:监听
const unsubscribe = bus.on('notification', (data) => {
console.log('收到消息:', data);
});
// 标签页 B:发送
bus.send('notification', { title: '新消息', content: 'Hello!' });
核心原理:利用 localStorage 的 storage 事件天然支持跨标签页通知的特性。写入 localStorage 时,同源的其他标签页会收到事件。发送后用 setTimeout 清理临时数据,避免 localStorage 被污染。这种方案比 BroadcastChannel 的兼容性更好(IE 也支持 storage 事件),但只能用于同源场景。
自我检测
读完本章后,对照下面的清单检验一下自己的掌握程度:
- 能列举 Cookie 的六个核心属性(Domain、Path、Expires/Max-Age、HttpOnly、Secure、SameSite)并解释每个的作用
- 能说清楚
SameSite的三个值(Strict、Lax、None)的区别,以及为什么 Lax 是默认值 - 能区分 localStorage 和 sessionStorage 的生命周期与作用域,特别是 sessionStorage 刷新不丢失、新标签页复制等细节
- 能说出 Web Storage 的三个主要局限(只能存字符串、同步 API、无过期机制)
- 能简要描述 IndexedDB 的核心特性(异步、事务、支持索引和二进制数据、大容量)
- 能在给定场景下选择合适的存储方案并说出理由
- 能解释为什么敏感信息不应该放在 localStorage 中,以及 Cookie 的 HttpOnly 属性如何防御 XSS
- 能利用 localStorage 的 storage 事件做简单的跨标签页通信
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90