
1. 先把这三个东西摆到台面上聊到前端数据存储绕不开的就是 localStorage、sessionStorage、cookie 这三个老熟人。面试的时候会被问实际开发中更是天天用但说句实话能把这仨的细节彻底说清楚的人真不多。比如就有人问过cookie 到底存不存在请求头里localStorage 怎么按 id 删数据为什么开发者工具里看不到 cookie这些看似基础的问题真到用的时候反而容易卡壳。本篇就当一次个人项目复盘把我这些年在这三个存储方案上踩过的坑、总结出来的规律、以及整理好的工具函数一次性倒出来。不管你是刚入行前端的新手还是干了几年想查漏补缺的老手这篇应该都能给你点实在的东西。全程没有啥高深理论都是能直接抄作业的写法。2. 认知模型三个容器三种脾气2.1 cookie 是“每次都要带上的身份证”cookie 的诞生时间最早它的核心使命其实和存储没啥关系而是为了帮服务器识别客户端状态。HTTP 协议本身是无状态的服务器不知道你上一次请求干了啥cookie 就是服务器发给浏览器的一张小纸条浏览器每次请求都会把这张纸条原样塞回给服务器。这就解释了为什么 cookie 会有 4KB 左右的体积限制也解释了为什么 cookie 会在请求头里出现。热词里有人问“cookie 是在请求头里吗”答案是cookie 既存在于请求头也存在于 document.cookie。请求头发出去的是给服务器看的document.cookie 是给 JS 脚本看的这两者底层是同一份数据只不过暴露的通道不同。有两点需要特别注意第一cookie 的读写支持跨域名配置通过 Domain 属性可以控制哪些域名能收到这张纸条第二cookie 的存储方向是双向的服务器也能通过 Set-Cookie 响应头往浏览器里种 Cookie。所以你要是只在前端用 document.cookie 操作它反而是把这个原生于“前后端协作”的机制给降级成了纯本地存储。2.2 localStorage 是“永不设限的保险柜”HTML5 时代给前端带来了 Web Storage其中 localStorage 是最常用的一个。它的特点非常鲜明数据持久保存在浏览器里没有过期时间除非用户手动清除或者你的代码主动删除否则它永远在那儿。它的 API 是同步的且非常简洁setItem、getItem、removeItem、clear这四个方法基本覆盖了全部需求。因为它只存在于浏览器端不参与网络请求所以也不会像 cookie 那样拖累请求体积。存储空间一般浏览器会给到 5MB 左右比 cookie 大得多但也别真的把它当数据库用。从开发者的角度localStorage 就像是给每一个源协议域名端口分配了一个独立保险柜。你在 http://a.com 存的东西在 http://b.com 是拿不到的这个同源限制一定要记牢否则跨域名联调的时候会一脸懵。2.3 sessionStorage 是“关标签页就销毁的临时纸片”sessionStorage 和 localStorage 的 API 几乎一模一样区别只有一个生命周期。sessionStorage 的生命周期被限定在一个标签页的会话中只要标签页还开着哪怕你刷新页面或者跳转到同源的其他页面数据都还在一旦标签页关闭数据立刻没了。这意味着 sessionStorage 非常适合存“既然这个会话还没结束就帮我临时记住”的数据。比如用户在填写一个多步骤表单每一步的草稿可以塞进 sessionStorage刷新也不会丢等他最终提交完或者关掉页面数据自然清理不需要你手动管。另外要注意如果你把一个页面复制成两个标签页这两个标签页的 sessionStorage 是彼此独立的复制出来的新标签页会拷贝原标签页的会话数据但在那之后两者就各走各的了。3. cookie 的完整操作细节与封装3.1 读取和写入的真实写法最原始的 cookie 读写方式其实就是读写字符串但大多数人从来不直接操作原始字符串而是封装成工具函数。先用最简单的方式说明原理// 写入一个cookie document.cookie tokenabc123; path/; max-age86400; // 读取全部cookie console.log(document.cookie); // 输出类似 tokenabc123; themedark你会注意到 document.cookie 返回的是一个以分号分隔的字符串而不是一个对象。而且每次写入一个 cookie 并不是覆盖全部而是追加或者更新其中某一项。这个特性在封装工具函数时非常关键。再比如删除一个 cookie标准做法不是调什么 API而是把它的过期时间改成过去的时间点// 删除cookie document.cookie token; path/; max-age-1;3.2 项目实战级 cookie 工具函数在实际项目中直接写上面这种原生代码很不利于维护我自己在项目里常用的一套封装长这样你可以直接拷走用const CookieUtil { // 设置cookie // key: cookie名value: cookie值days: 有效天数path: 生效路径domain: 域名 set(key, value, days 7, path /, domain ) { const expires days ? ; expires${new Date(Date.now() days * 864e5).toUTCString()} : ; document.cookie ${encodeURIComponent(key)}${encodeURIComponent( value )}${expires}; path${path}${domain ? ; domain${domain} : }; }, // 获取指定cookie get(key) { const cookies document.cookie.split(; ).reduce((res, item) { // 判断字符串是否包含等号避免解析到空项 if (item.includes()) { const parts item.split(); res[decodeURIComponent(parts.shift())] decodeURIComponent(parts.join()); } return res; }, {}); return cookies[decodeURIComponent(key)] ?? null; }, // 删除cookie remove(key, path /, domain ) { this.set(key, , -1, path, domain); }, }; // 用法 CookieUtil.set(username, 张三, 30); console.log(CookieUtil.get(username)); // 输出张三 CookieUtil.remove(username);这里有两个细节值得说第一所有存储的值我都做了 encodeURIComponent 编码就是为了解决中文乱码问题。热词里专门有“cookie 中文”这个词实际开发中如果不做编码中文字符在 cookie 里经常会出现显示异常或者传递失败的情况。第二get 方法里把等号后面的内容全部 join 回来了这是为了防止 value 本身包含等号导致解析出错这个坑我在项目里真实踩过。3.3 cookie 的属性到底怎么配置才合适cookie 的属性和权限控制密切相关我挑几个重点讲Expires / Max-Age过期时间前者是具体时间点后者是相对秒数。不设置的话cookie 就是会话级的浏览器一关就没了。Domain控制哪些域名能读取这个 cookie。设为 .example.com 的话子域名都共享。但千万别随手设成顶级域名容易引发安全问题。Path控制 cookie 在哪个路径下生效。设成 / 表示全站可用。Secure标记为 Secure 的 cookie 只能通过 HTTPS 协议传输本地 http 环境下测试时要留意否则明明种了却看不到。HttpOnly这个是给后端用的设置之后 JS 的 document.cookie 就读不到这个 cookie 了能有效降低 XSS 攻击风险。SameSite控制跨站请求时是否携带 cookie。设为 Strict 能防止很多 CSRF 攻击但也要考虑第三方登录跳转的兼容性问题。提示很多时候前端用 cookie 存登录信息但安全要求高一点的系统都应该让后端把认证 cookie 设置为 HttpOnly前端只负责后续请求携带它而不是去读它。这也是为什么你在 chrome 开发者工具里看不到某些 cookie 的原因之一——不是没存是 JS 被禁止访问了。4. localStorage 和 sessionStorage 的实操要点4.1 基础操作与 Storage 事件监听这两个的 API 是完全一样的简单列一下// 存 localStorage.setItem(token, abcdef); // 取 const token localStorage.getItem(token); // 删单个 localStorage.removeItem(token); // 清空 localStorage.clear();sessionStorage 的用法一模一样把前缀换成 sessionStorage 就行。不过在真实项目中直接存字符串的场景往往不够用因为我们经常要存对象。但 localStorage 原生只能存字符串所以必须手动做序列化和反序列化// 存对象 const user { name: 李四, age: 28 }; localStorage.setItem(user, JSON.stringify(user)); // 取对象 const userStr localStorage.getItem(user); const userObj userStr ? JSON.parse(userStr) : null;另一个容易被忽略的是 Storage 事件。当 localStorage 或 sessionStorage 中的数据发生变化时浏览器会触发 storage 事件但有一个限制这个事件只在其他标签页里触发触发页面本身是收不到的。window.addEventListener(storage, (event) { console.log(变化的key:, event.key); console.log(旧值:, event.oldValue); console.log(新值:, event.newValue); });这个特性的价值在于多标签页同步。比如你在一个标签页里登录了另一个标签页监听到 localStorage 变化后可以立刻刷新用户信息不需要依赖复杂的手写消息传递。4.2 按 id 删除 localStorage 数据的实战写法热词里有人专门搜“根据id删除localstorage数据”这在实际开发中确实是高频需求。比如你有一个列表每个 item 都有自己的 id你希望按 id 只删对应的缓存而不是清空全部。一个能够直接落地的写法如下// 假设存储的数据格式是 book_123、book_456、user_profile 这种带前缀的key function removeById(targetId) { // 要遍历所有的key Object.keys(localStorage).forEach((key) { // 判断key是否以book_开头 if (key.startsWith(book_)) { // 从key中提取id部分 const id key.split(_)[1]; if (id String(targetId)) { localStorage.removeItem(key); console.log(已删除: ${key}); } } }); } // 调用 removeById(123);如果你存储的不是分散的 key而是一个大数组那么更合理的做法是整体取出、过滤、再写回function removeItemFromList(listKey, itemId) { const raw localStorage.getItem(listKey); if (!raw) return; const list JSON.parse(raw); const newList list.filter((item) item.id ! itemId); localStorage.setItem(listKey, JSON.stringify(newList)); } // 调用 removeItemFromList(bookList, 123);这两种方式对应两种不同的数据组织结构第一种适合散列存储第二种适合集中式数组存储。从可维护性看我建议大规模业务数据尽量用第二种统一管理更容易控制。4.3 localStorage 的容量限制与超限处理很多人知道 localStorage 有 5MB 限制但没想过超限之后会发生什么setItem 会直接抛出一个 QuotaExceededError 异常。如果你不捕获它后果是页面脚本中断后续代码全部不执行。所以在写入关键数据时我习惯做一个安全封装function safeSetItem(key, value) { try { localStorage.setItem(key, value); } catch (e) { // 常见情况是超出容量限制 console.error(存储失败:, e); // 这里可以做一些降级处理比如裁剪老数据 pruneOldData(); } }容量测试的快速方法写一个循环往里面塞字符串直到报错为止看看你的浏览器到底给了多少空间。不同浏览器结果不一样别指望 5MB 是统一的硬指标。5. 三个存储方案的选型对比5.1 从使用场景反推技术选型到现在为止我已经把三个存储方案的核心机制和 API 都过了一遍。但真正到项目里你不可能只看 API 就决定用哪个还是得看场景需求。我把我的选型经验整理成了下面的对比表配合具体场景来解读维度cookielocalStoragesessionStorage容量约4KB约5MB约5MB生命周期可设置过期时间或会话级持久保存无过期关闭标签页即失效参与请求自动携带在请求头中不参与请求不参与请求作用域可跨子域名共享同源窗口共享当前标签页独享存储类型字符串需编码字符串可序列化字符串可序列化主要用途会话标识、用户追踪长期缓存、偏好设置、离线数据临时表单、页面状态、会话内草稿安全风险易受XSS/CSRF攻击易受XSS攻击易受XSS攻击5.2 具体场景下的选择建议如果我遇到下面这些情况通常是这样选的存登录凭证 token优先让后端设置 HttpOnly cookie前端只负责访问接口时自动携带。如果后端比较老不支持退而求其次存 localStorage然后每次请求前手动附上 Authorization 头。千万不用 sessionStorage用户一开新标签页就掉线体验太差了。存购物车数据用 localStorage。购物车往往是跨会话、跨页面的用户关掉浏览器再打开还希望能看到。而且购物车数据量不大5MB 绰绰有余。如果多标签页购物车需要实时同步配合前面说的 storage 事件就能实现。存表单草稿用 sessionStorage。用户填到一半刷新了、误关了标签页导致内容丢失这体验确实不好。但你又不想让草稿永久留在浏览器里这时候 sessionStorage 就是最合适的方案。存用户主题偏好、语言设置用 localStorage。这是典型的长期偏好打开页面时读取一次设置后永久生效。存页面试探性状态比如某个提示是否展示过用 localStorage 还是 sessionStorage取决于“是否希望下次打开浏览器还记住”。记住就用前者仅仅本次会话记住就用后者。6. 经典问题排查与踩坑记录6.1 开发者工具里看不到 cookie怎么回事有人搜“chrome开发者工具没有cookie”这个问题我给自己排查过好多次。常见原因有三个cookie 设置了 HttpOnlyApplication 面板的 Storage - Cookies 里能看到但 JS 没法读到不属于前端逻辑的问题。cookie 被 SameSite 或者 Secure 属性拦了本地用 http 测试 Secure cookie 时登录接口种的 cookie 根本不会落下来。你使用了隐私模式或者第三方插件把所有 cookie 禁掉了。排查思路很简单打开开发者工具的 Network 面板看请求的响应头 Set-Cookie 或请求头 Cookie如果这里能看到就说明 cookie 机制正常工作只是你看的位置不对。问题往往出在 Domain、Path、Secure 这些属性上逐项检查即可。6.2 cookie 为什么没生效这类问题最常发生在跨域场景。你在 a.example.com 种了一个 Domain 为 a.example.com 的 cookie然后在 b.example.com 去请求它当然不会带。所以判断 cookie 是否该带上的时候第一件事就是看当前访问的域名和 cookie 的 Domain 是否匹配。另一个高频问题是前端种 cookie 时没写 Path导致默认 path 为当前页面路径。比如当前页面是 /admin/login种下的 cookie 只在 /admin/login 路径下有效你跳到 /admin/dashboard 就没了。所以前端种 cookie 时必须显式设置 path/这是我在项目里踩过最多次的坑之一。6.3 js 怎么判断字符串是否包含关键字这个热词和存储本身没直接关系但和 localStorage 的前缀匹配、cookie name 解析确实有关联。JavaScript 里判断字符串包含一般有三种方式const str book_123_notes; // 方式一includesES6引入最常用 const hasPrefix str.includes(book_123); // 方式二indexOf老项目里常见 const idx str.indexOf(book_123); if (idx ! -1) { // 包含 } // 方式三正则表达式 const hasMatch /book_123/.test(str);includes 语义最清晰indexOf 兼容性最好正则适合复杂模式匹配。结合我们前面写的 removeById 函数判断 localStorage 中哪些 key 需要删除本质上就是判断 key 字符串是否包含目标模式。6.4 容量超限和数据污染问题localStorage 虽然容量比 cookie 大但很容易被人忽略的是它并没有自动清理功能。长期跑下来的业务如果不做版本管理数据格式一变就会出问题。比如你之前存的是数组现在改成对象了旧的缓存结构读出来之后 JSON.parse 可能报错或者解析出无法识别的结构。所以我在项目里会给缓存加一个版本号后缀user_v1、user_v2读取时只认当前版本旧版本直接删除重写。这是一种非常廉价但有效的缓存迁移策略。另外凡是读取 localStorage 的操作全都要包一层 try-catch因为用户可能手动修改数据、第三方脚本可能篡改数据所有不可信的输入都应当被防御性处理。7. 封装一套通用本地存储工具基于上面这些经验我在项目里经常会维护一套统一封装把 localStorage 和 sessionStorage 的常用操作集中管理同时自动处理 JSON 序列化和异常捕获。这套方案本身就是我从这个项目中沉淀下来的最大收获。const StorageEngine { // 带过期时间的本地存储 // key: 存储键value: 存储值expires: 过期时间(毫秒)默认7天 set(key, value, expires 7 * 24 * 3600 * 1000) { const payload { data: value, expires: Date.now() expires, }; localStorage.setItem(key, JSON.stringify(payload)); }, // 读取如果过期返回null并自动清理 get(key) { const raw localStorage.getItem(key); if (!raw) return null; try { const payload JSON.parse(raw); if (payload.expires Date.now() payload.expires) { localStorage.removeItem(key); return null; } return payload.data; } catch (e) { // 解析失败直接清理脏数据 localStorage.removeItem(key); return null; } }, // 删除 remove(key) { localStorage.removeItem(key); }, }; // 用法 StorageEngine.set(userProfile, { name: 王五 }, 3600 * 1000); const profile StorageEngine.get(userProfile);这套封装里把“带过期时间”这个能力做进去了弥补了 localStorage 没有过期机制的短板。很多业务数据本质上是有时效的比如活动配置、接口缓存、临时令牌手动打时间戳太麻烦封装一层就一劳永逸。注意这里的过期时间是前端软逻辑用户清缓存或者换设备后依然无效。真正对安全有要求的场景时效校验必须靠后端而不是前端。还有个加分项尽量把所有的存储 key 统一收敛到一个常量文件里避免散落在各个业务文件中。比如export const STORAGE_KEYS { TOKEN: access_token, USER_INFO: user_info_v1, CART_LIST: cart_list_v2, THEME_MODE: theme_mode, };统一管理的核心价值在于你以后想改某个 key 的命名规则、想升级缓存版本、想统计哪些数据被存了都只需要改一处就好。最后再说一个个人经验虽然 localStorage 和 sessionStorage 用起来非常简单但在高安全场景如支付、个人隐私数据里要慎重任何存到前端的敏感数据都有可能被 XSS 窃取。我在实际开发里的底线是密码绝不存前端token 尽量走后端 HttpOnly cookie非敏感的用户偏好才放 localStorage。把握好这条线你在这三个方案上的知识基本就过关了。