你有没有想过:刷新一下页面,购物车还在、主题色没变、登录状态也没掉——这些数据都存在哪?答案就是浏览器的各种存储。本文逐个介绍 localStorage、sessionStorage、Cookie 和 IndexedDB 的用法与适用场景,并带你做一个主题切换 + 跨标签页同步的完整小案例,学完你就知道"数据该往哪儿放"。
1. 先认识存储家族
浏览器提供的存储手段很多,选错了会带来麻烦:数据存不下、被意外清掉、或者每发一次请求都背上沉重的 Cookie。先看一张对比表:
| 存储 | 容量 | 持久性 | 特点 |
|---|---|---|---|
| localStorage | 约 5MB | 永久 | 同步 API,简单易用 |
| sessionStorage | 约 5MB | 标签页关闭即清 | 每个标签页独立 |
| Cookie | 约 4KB | 可设过期时间 | 每次请求自动携带 |
| IndexedDB | 数百 MB 以上 | 永久 | 异步,支持索引与事务 |
选型口诀:小数据、要持久、纯前端用 → localStorage;只在本标签页临时用 → sessionStorage;需要服务器读取 → Cookie;数据量大、结构复杂 → IndexedDB。
2. localStorage 基础操作
localStorage 的 API 只有 4 个方法,所有值都会以字符串形式保存。注意一个坑:数字存进去再取出来是字符串,别拿它直接做加法运算,否则会得到字符串拼接的结果。
localStorage.setItem('username', 'linwan');
console.log(localStorage.getItem('username'));
console.log(localStorage.getItem('not-exist'));
localStorage.removeItem('username');
localStorage.setItem('a', '1');
localStorage.setItem('b', '2');
console.log(localStorage.length);
localStorage.clear();
getItem 找不到键时返回 null,所以判断"是否存在"用 !== null 比直接 if 更严谨,能区分"不存在"和"存了空字符串"两种情况。clear() 会清空当前域名下的全部数据,生产代码里慎用。
localStorage 还有两个容易被忽略的点:一是它按"协议 + 域名 + 端口"隔离,http://a.com 和 https://a.com 的数据互不相通,本地调试时用 localhost 和用 127.0.0.1 也互相看不见;二是容量约 5MB,写满会抛 QuotaExceededError,所以写入时最好包一层 try/catch,存不下时给用户降级提示,而不是让页面静默出错。
一个实用技巧:存数据前先估算大小,JSON.stringify(obj).length 就是这条记录占用的字符数,心里有数就不会突然撞上限;如果发现快满了,可以按写入时间清理最旧的数据,很多"缓存类"功能的淘汰策略就是这么写的。
3. 存对象:先序列化再反序列化
localStorage 只能存字符串,对象要先用 JSON.stringify 变成字符串,读取时再用 JSON.parse 还原。下面是一个待办事项的存取封装,顺便演示了带前缀的键名和容错解析:
const todo = { id: 1, text: '学完浏览器存储', done: false };
function saveTodo(todo) {
localStorage.setItem('todo:' + todo.id, JSON.stringify(todo));
}
function loadTodo(id) {
const raw = localStorage.getItem('todo:' + id);
if (raw === null) {
return null;
}
try {
return JSON.parse(raw);
} catch (err) {
console.error('数据损坏:', err);
return null;
}
}
saveTodo(todo);
console.log(loadTodo(1));
console.log(loadTodo(999));
用 todo:{id} 这种带业务前缀的键名,可以避免不同模块的数据互相覆盖。JSON.parse 包在 try/catch 里也很有必要——用户手改过数据、或者旧版本代码写入过非法 JSON 时,直接解析会抛异常,整个页面跟着崩。
想遍历所有已存的键,Object.keys(localStorage) 就能拿到键名数组——localStorage 实现了 Storage 接口,支持用对象的方式访问。给每个键加上业务前缀还有个额外好处:清理时按前缀过滤,只删自己模块的数据,不会误伤其他功能。真实项目里建议把存取封装成统一的模块,暴露 get/set/remove 三个函数,内部处理序列化和异常,业务代码永远不直接碰 localStorage。
4. sessionStorage 与 Cookie
sessionStorage 的 API 和 localStorage 完全一样,区别只在生命周期:它绑定当前标签页,标签页一关数据就没了。典型用途是"表单填写到一半,误刷新不丢"这类临时状态。Cookie 则完全不同——它会被浏览器自动附在每次请求头上,所以容量被限制得很小:
sessionStorage.setItem('draft', '正在写的草稿');
console.log(sessionStorage.getItem('draft'));
function getCookie(name) {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = document.cookie.match(
new RegExp('(?:^|; )' + escaped + '=([^;]*)')
);
return match ? decodeURIComponent(match[1]) : null;
}
document.cookie = 'theme=dark; max-age=3600';
console.log(getCookie('theme'));
Cookie 的读写都是字符串操作:document.cookie 一次只能写一个键值对,读出来却是所有 Cookie 拼成的大字符串,所以要写解析函数。代码里那串正则先把键名里的特殊字符转义,再精确匹配,max-age=3600 表示 1 小时后过期。
现代开发里,JS 直接读写 Cookie 的场景越来越少,因为安全问题太多了。重要的 Cookie 应该加上 HttpOnly(禁止 JS 读取,防 XSS 窃取)、Secure(只走 HTTPS)、SameSite(防 CSRF)——而这些属性只能由服务器通过 Set-Cookie 响应头设置,前端 JS 碰不到。这也是为什么登录态一般交给服务端管理:前端只负责把服务器返回的 token 妥善保存,别自己往 Cookie 里塞敏感信息。
5. IndexedDB:浏览器的"数据库"
当数据是成百上千条记录、还要按条件查询时,localStorage 就力不从心了。IndexedDB 是浏览器内置的 NoSQL 数据库,支持索引、事务和游标。它的 API 偏底层,先封装一层打开数据库的逻辑,后面用起来就顺手了:
function openDB() {
return new Promise(function (resolve, reject) {
const request = indexedDB.open('codelab-db', 1);
request.onupgradeneeded = function () {
const db = request.result;
if (!db.objectStoreNames.contains('todos')) {
db.createObjectStore('todos', { keyPath: 'id' });
}
};
request.onsuccess = function () {
resolve(request.result);
};
request.onerror = function () {
reject(request.error);
};
});
}
indexedDB.open(name, version) 打开数据库;第一次打开或版本号变大时会触发 onupgradeneeded,在这里建表(官方叫"对象仓库 object store")。keyPath: 'id' 表示用每条记录的 id 字段做主键,相当于关系数据库里的主键。
写入数据要走"事务":事务提交成功才算真正落盘,中途出错会自动回滚。读取是异步的,通过 onsuccess 拿结果:
async function addTodo(todo) {
const db = await openDB();
return new Promise(function (resolve, reject) {
const tx = db.transaction('todos', 'readwrite');
tx.objectStore('todos').add(todo);
tx.oncomplete = function () {
resolve();
};
tx.onerror = function () {
reject(tx.error);
};
});
}
async function getTodo(id) {
const db = await openDB();
return new Promise(function (resolve, reject) {
const request = db.transaction('todos')
.objectStore('todos').get(id);
request.onsuccess = function () {
resolve(request.result);
};
request.onerror = function () {
reject(request.error);
};
});
}
addTodo({ id: 1, text: '搞定 IndexedDB', done: false })
.then(function () {
return getTodo(1);
})
.then(function (todo) {
console.log('读到了:', todo.text);
});
读操作用只读事务就够了,性能更好;写操作必须用 readwrite。另外提醒一点:每次操作完记得 db.close(),大量标签页同时打开时,连接数有限,不关会耗尽连接导致后续请求排队。
IndexedDB 的能力不止增删改查:建表时可以给某个字段声明 index,之后就能按这个字段快速查询;一次事务里可以连续操作多条记录,要么全部成功、要么全部回滚;数据量大的列表用 getAll() 配合游标分页读取,比"一次全读进内存"稳得多。第一次接触觉得它繁琐很正常,现在有很多库(如 idb、Dexie)把它的 Promise 封装做得很好,但理解底层的事务模型依然重要——出问题时你才知道去哪查。
6. 实战:主题切换与跨标签页同步
把前面学的串起来:主题偏好存 localStorage,页面加载时读取并应用;再用 storage 事件监听其他标签页的修改,实现"一个标签页改主题,所有标签页同步变"的效果:
const select = document.getElementById('theme-select');
const saved = localStorage.getItem('theme') || 'light';
document.documentElement.dataset.theme = saved;
if (select) {
select.value = saved;
select.addEventListener('change', function () {
document.documentElement.dataset.theme = select.value;
localStorage.setItem('theme', select.value);
});
}
window.addEventListener('storage', function (event) {
if (event.key === 'theme' && event.newValue) {
document.documentElement.dataset.theme = event.newValue;
if (select) {
select.value = event.newValue;
}
}
});
storage 事件只在"其他标签页"修改 localStorage 时触发,当前页面自己改不会触发,所以不用担心死循环。dataset.theme 会变成 data-theme 属性,配合 CSS 的属性选择器,就能用一套 CSS 实现多套主题。
最后提醒一个环境问题:localStorage、sessionStorage 和 indexedDB 都只存在于浏览器环境,在 Node.js 或服务端渲染(SSR)里直接调用会报 ReferenceError。如果代码要在多种环境跑,先判断 typeof window !== 'undefined' 再访问;某些隐私模式还会禁用存储,访问时同样要容错。把"存储可能不可用"当成默认假设,代码才够健壮。
7. 总结与练习
四种存储各有定位:localStorage 存持久小数据,sessionStorage 存临时状态,Cookie 用于和服务器交换身份信息,IndexedDB 扛大数据量。还要记住:所有存储都有被用户清除的可能,读取时务必做容错处理。
练习:
- 做一个待办列表页:增删改查都落到 localStorage,刷新后数据不丢。
- 把同一份待办数据迁移到 IndexedDB,对比两种方案在数据量 1000 条时的表现。
- 用 sessionStorage 实现"草稿自动保存":输入框内容变化即保存,刷新后自动恢复。
💡 在 DevTools 的 Application 面板可以可视化查看、编辑甚至删除所有存储数据,调试存储问题比 console.log 高效十倍。