做前端这么多年cookie 这个东西几乎天天碰但说实话很多人对它的理解停留在“document.cookie xxx”这种程度。真要问起来路径为什么失效、中文为什么乱码、对象怎么存、删除为什么删不掉能一口气说清楚的人并不多。这篇博客不聊库、不聊框架就用纯 js 把前端读写 cookie 这件事彻底讲明白从原理到封装再到排坑全部过一遍。这篇文章适合谁看刚入门想搞懂 cookie 机制的前端新手被 cookie 路径和编码问题折磨过的开发备前端面试被问到“cookie 和 localStorage 有什么区别”的求职者都应该能从中拿到点东西。核心内容不依赖任何框架原生 JavaScript 直接可跑看完就能用到自己的项目里。1. cookie 在今天的开发里为什么还在用1.1 cookie 的身份地位比想象中更重要先给个结论虽然现在有了 localStorage、sessionStorage、IndexedDB 这些花里胡哨的浏览器存储方案但 cookie 在 web 开发里依然不可替代。原因是它的一个独特本质——cookie 会在每次 HTTP 请求时自动携带到服务端。也就是说cookie 不光是浏览器本地存储它还是前端和服务端之间传递状态的一种约定。举几个最常见的场景用户登录后服务端下发一个 sessionId 存到 cookie之后每次请求自动带上服务端就能识别当前是谁。埋点统计把用户标识 uid 写进 cookie上报数据时一起发到服务端。偏号设置比如用户切换了深色模式、改了语言写进 cookie下次访问依然能记住。A/B 实验分组用户被分到哪一组需要持久化cookie 是默认的方案。你会发现凡是需要“服务端也能读到的前端状态”基本都靠 cookie 来完成。localStorage 仅存前端请求不会自动带上除非你用 JS 手动塞到 header 里但这会引入 CSRF 等一堆安全风险没人会这么干。1.2 cookie 和其他存储方案怎么选前端常用的几种存储我直接用一张表说明白面试也常考这个对比存储方案容量请求自动携带生命周期前端可脚本读取cookie单个约 4KB是可设置不设则为会话级可HttpOnly 除外localStorage约 5-10MB否永久需手动清理可sessionStorage约 5-10MB否标签页关闭即失效可IndexedDB很大GB 级否持久可如果你只是想在浏览器本地存点非敏感数据比如用户偏好、草稿内容优先用 localStorage容量更大、API 更简单、请求不带出去还省流量。但如果这个数据需要服务端在请求时自动获取或者需要遵循 cookie 的过期策略、安全策略那就只能用 cookie。所以我的建议是不要纠结“谁替代谁”它们在各自的位置上都有存在的理由。前端开发者的基本功是每种方案都明白它的机制和坑然后按需选型。2. 读懂 cookie 的构成再谈读写2.1 document.cookie 到底做了什么纯 js 操作 cookie核心就一个接口document.cookie。它有两个身份读的时候是当前页面可访问的所有 cookie 拼接而成的字符串写的时候是设置一个新 cookie 的入口。这种“读写同属性”的设计在 JS 里很少见也导致很多新手一开始比较懵。读取时document.cookie返回的格式大概是这样的name1value1; name2value2; name3value3注意几个细节每个 cookie 之间用分号加空格分隔。返回的只是键值对不包含过期时间、路径、domain 等元信息这些信息浏览器不会通过这个 API 吐给你。返回的是当前页面有权访问的所有 cookie。什么叫有权取决于 cookie 的 path 和 domain 属性稍后细讲。默认情况下cookie 里的值不经过解码就是原始的编码状态。所以你自己写进去的时候用 encodeURIComponent 编码读的时候必须手动 decodeURIComponent 解码。写入时document.cookie namevalue; expires...; path...; domain...; secure。这里有个反直觉的点用赋值并不会覆盖掉之前的 cookie而是追加一个新 cookie。同一 name 的 cookie 会被更新但不同 name 的 cookie 会共存。也就是说document.cookie的写操作本质是“增量写入”不是“整体替换”。很多人对 cookie 的存储位置有误解以为它在浏览器某个目录里以文本文件存在。实际上现代浏览器把 cookie 存储在本地数据库文件中Chrome 用的是 SQLite 格式但这些底层细节对前端开发者是透明的你只需要通过 document.cookie 或 DevTools 的 Application 面板来操作和查看即可。2.2 每一个属性字段的取舍逻辑设置 cookie 时可以在分号后面追加属性每个属性都会影响 cookie 的行为。逐个拆解expires过期时间UTC 字符串格式。过了这个时间cookie 会被浏览器自动清除。如果不设置cookie 的生命周期是“会话级”即浏览器标签页关闭就失效。max-age相对存活时间单位秒。表示 cookie 从设置时刻起最多存活多少秒。它和 expires 同时存在时max-age 优先级更高。现代开发建议用 max-age因为它不用计算具体日期更直观。path生效路径。默认是当前页面的路径。这个是坑王之王很多“cookie 明明设置了但读不到”的问题都是 path 搞的鬼。domain生效域名。默认是当前域名不包含子域名。如果你想让a.example.com和b.example.com共享一个 cookie需要显式设置domainexample.com。secure布尔属性只需写名字不需要赋值。设置后cookie 只在 HTTPS 连接下才会被发送。本地开发用 http://localhost 时localhost 被浏览器特殊对待通常也能正常测试。sameSite控制第三方上下文的发送策略取值 Strict、Lax、None。这已经是现代 web 安全里绕不开的字段了。默认行为在不同浏览器中略有差异现代 Chrome 默认 Lax效果是用户在地址栏输入网址访问时携带 cookie但通过链接跳转的跨站场景下限制部分携带。说个我自己的习惯但凡设置 cookiepath 一定显式写成/。因为默认机制是“当前路径”你要是恰好在一个子路由页面设置了 cookie就会发现其他页面全读不到排查半天最后发现是 path 的问题。这是个成本极低但回报极高的好习惯。2.3 为什么写 cookie 之前一定要编码cookie 的值有几个字符限制分号、逗号、空格等特殊字符会导致 cookie 解析异常。比如你存一个用户的昵称叫“张三; admin”这个分号会直接把 cookie 截断成两个部分后面部分全部丢失。解决方案就是用encodeURIComponent编码值读取时用decodeURIComponent解码。这等于把特殊字符转成%XX格式浏览器和 JS 都能正确处理。同理中文也必须编码不然有些浏览器会直接报错或者乱码。我把编码这个点单独拎出来说是因为它属于“不会报错但逻辑出错”的隐形问题。你不编码大部分简单值跑得好好的但一旦遇到中文、特殊符号、JSON 字符串就翻车了。而翻车的表现还特别隐蔽不是立刻报错而是数据变成乱码或丢失。3. 纯 js 读写 cookie 的完整封装实战3.1 从零手写一个能用在生产环境的封装现在进入正题。下面这套封装是我自己项目里在用的去掉了框架依赖纯原生 JavaScript逻辑很直白每一行都有注释。别直接复制就完事建议自己敲一遍理解每个参数从哪里来、到哪里去。const CookieUtil { // 设置 cookie // name: cookie 名 // value: cookie 值内部自动编码传对象也行会被序列化 // days: 存活天数不传则默认会话级 // options: 额外配置 path、domain、secure、sameSite set(name, value, days 0, options {}) { const { path /, domain , secure false, sameSite Lax } options; // 值统一走编码对象序列化成 JSON 字符串再编码 let encodedValue; if (typeof value object) { encodedValue encodeURIComponent(JSON.stringify(value)); } else { encodedValue encodeURIComponent(value); } let cookieStr ${encodeURIComponent(name)}${encodedValue}; // 过期时间 if (days 0) { const expires new Date(Date.now() days * 24 * 60 * 60 * 1000); cookieStr ; expires${expires.toUTCString()}; } else if (days 0) { // days 传负数直接走删除逻辑 const expires new Date(0); cookieStr ; expires${expires.toUTCString()}; } cookieStr ; path${path}; if (domain) { cookieStr ; domain${domain}; } if (secure) { cookieStr ; secure; } if (sameSite) { cookieStr ; SameSite${sameSite}; } document.cookie cookieStr; return cookieStr; }, // 获取 cookie 值 // 返回解码后的字符串如果是对象序列化存的自动转回对象 get(name) { const cookieArr document.cookie.split(; ); for (let i 0; i cookieArr.length; i) { const [rawKey, ...rest] cookieArr[i].split(); const key decodeURIComponent(rawKey); if (key name) { const value decodeURIComponent(rest.join()); // 尝试把 JSON 字符串转回对象失败则原样返回字符串 try { return JSON.parse(value); } catch (e) { return value; } } } return null; }, // 删除 cookie remove(name, options {}) { this.set(name, , -1, options); }, // 获取全部 cookie返回对象 getAll() { const cookieArr document.cookie.split(; ); const result {}; cookieArr.forEach((cookie) { if (!cookie) return; const [rawKey, ...rest] cookie.split(); const key decodeURIComponent(rawKey); const value decodeURIComponent(rest.join()); result[key] value; }); return result; } };这套封装解决的核心问题有四个编码解码全自动调用方不用操心特殊字符和中文。对象可以直接存取内部用 JSON 序列化取出来自动解析回对象。删除不是单独写一段逻辑而是复用 set把 days 传负数设置一个过去的时间点让浏览器立刻清除。默认 path 设为/规避最常用的路径坑。3.2 过期时间的计算与边界过期时间是 cookie 最容易算错的地方。这背后的核心是expires属性接收的是UTC 字符串不能用new Date()直接拼。很多人写的时候直接用new Date().toISOString()这是错的——toISOString()返回的是带毫秒的 ISO 格式Cookie 的标准格式类似Wed, 21 Oct 2026 07:28:00 GMT虽然某些浏览器兼容 ISO但标准应该用toUTCString()。比如你想让 cookie 活 7 天const days 7; const expires new Date(Date.now() days * 24 * 60 * 60 * 1000); document.cookie tokenabc; expires${expires.toUTCString()}; path/;这里Date.now()拿到的是当前毫秒时间戳7 天的毫秒数是7 * 24 * 60 * 60 * 1000相加后就是第 7 天后的时间点。之所以用这种方式而不是new Date(2026-01-01)是为了保证过期时间是相对当前时间的代码在任意时刻执行都正确。那max-age怎么写更简单// 存活 7 天 document.cookie tokenabc; max-age${7 * 24 * 60 * 60}; path/; // 会话级 cookie不设置 expires 和 max-age document.cookie tokenabc; path/; // 立即删除max-age0 document.cookie tokenabc; max-age0; path/;实测下来如果你不需要计算具体到期日期用 max-age 更省心不用管时区、UTC 格式这些细节。但对老版本浏览器的兼容性expires 更稳妥。我个人在浏览器环境会优先用 expires在 Node 服务端中间件场景会基于框架默认行为处理。还有一个边界如果 days 传了小数会怎样比如days 0.5那expires就是 12 小时后到期。这其实是合理行为不等于报错。但如果你传的是NaN整个 cookie 字符串会变成非法值浏览器会默默忽略这次写入不报任何错。所以封装里最好加一层参数校验比如判断typeof days ! number就 return。3.3 真实业务场景登录状态存储与主题偏好封装写完了光看代码不够带入真实场景才知道怎么用。我挑两个有代表性的场景展开。场景一登录态存储用户登录成功后服务端返回一个 token 和过期时间前端把它写到 cookie 里。这里有两个选择本地算过期天数或者直接用服务端返回的过期时间。简单做法是服务端返回一个expiresIn字段单位秒。比如 7200 秒 2 小时前端换算成天数const expiresInSeconds 7200; // 服务端返回 const expiresInDays expiresInSeconds / (24 * 60 * 60); // 约等于 0.083 天 CookieUtil.set(access_token, response.token, expiresInDays);但更稳妥的做法是直接用服务端下发的绝对过期时间。服务端有时会返回expires_at这种具体时间戳前端就不需要自己换算天数了// 服务端返回 expires_at 为毫秒级时间戳 function setTokenWithExpiresAt(name, token, expiresAt) { const secondsLeft Math.max(0, Math.floor((expiresAt - Date.now()) / 1000)); const cookieStr ${encodeURIComponent(name)}${encodeURIComponent(token)}; max-age${secondsLeft}; path/; document.cookie cookieStr; }用 max-age 的优势在这就体现出来了不需要把剩余时间换算成天数直接上秒数。而且Math.max(0, ...)保证了过期时间已经过去时不会写出负数 max-age浏览器虽然也兼容但保险起见还是自己处理一下。场景二主题偏好用户切换深色模式前端把偏好存到 cookie。这里考验的是对象存取// 存主题偏好 const themeSettings { mode: dark, accentColor: #1890ff, fontSize: 14 }; CookieUtil.set(theme_preference, themeSettings, 30); // 存 30 天 // 读回主题偏好 const savedTheme CookieUtil.get(theme_preference); if (savedTheme) { document.documentElement.setAttribute(data-theme, savedTheme.mode); }因为封装内部自动做了序列化存进去的是encodeURIComponent(JSON.stringify(themeSettings))读出来JSON.parse还原成对象。如果 get 的时候解析失败会 fallback 返回原始字符串不会抛异常搞崩页面。这种容错在实际开发里非常重要——因为你永远不知道用户或者上一个开发者往 cookie 里塞了什么奇怪的值。4. 常见问题与排查技巧实录4.1 cookie 问题速查表先把最常踩的坑汇总成一张表快速定位问题方向现象大概率原因解决方案设置了 cookie但其他页面读不到path 默认是当前路径设置时显式加path/中文或特殊符号乱码/丢失未编码或编码不完整统一encodeURIComponent写入decodeURIComponent读取存对象/数组取出来是[object Object]直接document.cookie obj obj先JSON.stringify再编码存入cookie 删不掉删除时的 path 和设置时不一致删除必须带上和写入时相同的 path、domain设置后刷新页面失效没设置 expires 或 max-age需要持久化时必须设置过期时间获取的值里带%乱码读的时候忘了解码读取时手动decodeURIComponent页面访问时 cookie 没带上HttpOnly 限制或 SameSite 限制HttpOnly 的 cookie 前端读不到属于正常行为SameSite 需按场景调子域名之间 cookie 不互通未设置 domain顶级域名下设置domainexample.com4.2 重点问题展开路径失效、乱码、对象存取路径失效这是 cookie 新手最经典的翻车现场。假设你在https://example.com/dashboard/settings这个页面执行了document.cookie themedark没有写 path。这个 cookie 的实际生效路径是/dashboard/settings也就是说只有/dashboard/settings及其子路径能读到它。你再去https://example.com/home这个 cookie 就像消失了一样。原理在于浏览器判断一个 cookie 是否能发送/访问依据是请求的路径是否匹配 cookie 的 path。cookie 的 path 默认继承当前页面的目录所以你在admin/users页面设置cookie 就在admin/users目录生效。你要想让整个域名都生效必须显式写path/。中文和特殊字符的乱码cookie 的标准规范里value 本身允许的字符集是受限的。虽然浏览器实际实现时容忍度很高但保守做法就是所有非 ASCII 字符全部编码。我的习惯是无论是 key 还是 value写入前统一 encodeURIComponent读取前统一 decodeURIComponent。封装里这么做了生产环境基本不会因为编码问题出 bug。有个细节document.cookie.split(; )之后每个元素形如namevalue但你 value 里如果有号比如 JWT token、Base64 字符串直接split()会把后面的都丢掉。我封装里用了一个技巧const [rawKey, ...rest] cookieArr[i].split()然后把 rest 用join()拼回去。这个细节能省掉你排查半天“token 怎么少了后半截”的烦恼。对象存取cookie 里存字符串没问题存复杂对象就需要Serialize。为什么直接存会变成[object Object]因为对象隐式调用toString()时就是这个结果。正确的姿势是JSON.stringify(obj)存进去JSON.parse取出来。但 fetch 回来的值不一定是 JSON 格式所以读取时要做 try-catch解析失败就当普通字符串返回。补充一点实践心得cookie 不要存大对象。单个 cookie 大小限制约 4KB一个稍微复杂点的对象 JSON 化之后很容易超。而且 cookie 一发就是全量发到服务端太大很浪费带宽影响接口性能。复杂的、前端专用的数据交给 localStoragecookie 里只放必要的状态标识。4.3 调试 cookie 的实用技巧遇到“cookie 莫名其妙”的问题先别急着写 console.log 输出 document.cookie而是打开 DevTools 的 Application 面板。Chrome 里路径是DevTools - Application - Storage - Cookies点开左侧的域名右侧能看到当前域名下所有 cookie 的完整信息包括 Name、Value、Domain、Path、Expires、Size、HttpOnly、Secure、SameSite 每一列都清清楚楚。这个面板能直接看到 cookie 的完整生命周期和生效范围很多问题一眼就能定位。比如你发现页面上某个请求带了 cookie A但 document.cookie 读不到 A——那基本可以断定这个 cookie 是 HttpOnly 的前端脚本本来就无权访问属于正常行为。我自己排查 cookie 问题时的套路是先用 Application 面板确认 cookie 是否存在、属性和预期是否一致。再在 Console 里执行 document.cookie 看能不能读到。如果面板里有但 console 读不到那就是 HttpOnly。检查请求头里的 Cookie 字段看实际发送了什么。如果请求头里没有那就是 path 或 domain 不匹配或者 SameSite 限制。最后一招是临时给 set 去掉所有高级属性只留 namevalue看能不能读。能读就说明是某个属性配置问题逐个加回去定位。这套流程解决了我遇到的绝大多数 cookie 问题效率比瞎猜高得多。5. cookie 的安全边界与浏览器限制5.1 HttpOnly、SameSite、Secure 怎么用这几个属性是前端必须理解的尤其做登录场景时绕不开。不说多深的攻防但至少要明白它们分别防什么。HttpOnly标记了这个属性的 cookie 无法被 JavaScript 的 document.cookie 读取。它存在的核心目的是防止 XSS 攻击。如果攻击者在你页面里注入了一段恶意 JS想偷走你的登录态HttpOnly 会让这段 JS 读不到 cookie偷了个寂寞。需要提醒的是HttpOnly 是服务端设置并通过响应头Set-Cookie下发的能力前端 JS 通过 document.cookie 设置 cookie 是不能加 HttpOnly 的。因此在前后端分离的架构下登录 token 往往有两种存放思路一是服务端下发 HttpOnly cookie前端无感靠浏览器自动携带二是前端拿到 token 自己存 cookie但这样防御面就窄了一些。Secure只有 HTTPS 连接下才发送这个 cookie。你如果在本地 http 环境调试默认 localhost 是例外可以正常发但部署到公网环境如果没有 HTTPSSecure cookie 就永远发不到服务端。生产环境强烈建议给涉及登录态的 cookie 都加上 Secure。SameSite解决的是 CSRF 和第三方请求携带 cookie 的问题。三个取值Strict任何跨站请求都不带 cookie。最安全但用户体验有代价——比如从外站链接进入你的站点初始请求不会带登录态用户看起来像“未登录”一样。Lax现代浏览器的默认值。跨站的基础请求比如用户自己点击链接跳转会带 cookie但跨站 POST、iframe 等场景不会带。None所有跨站请求都带 cookie但前提是必须同时设置 Secure否则浏览器拒绝。实际业务中如果遇到“A 站 iframe 嵌入 B 站时B 站的 cookie 不被发送”这种问题大概率是 SameSite 默认限制所致。要放行需要 B 站把 cookie 的 SameSite 设为 None且 B 站必须跑在 HTTPS 下。5.2 跨域与第三方 cookie 的现代约束严格来说cookie 天然受同源策略保护但浏览器实现时做了一定的松绑只要 domain 和 path 匹配跨端口、跨协议也是允许的。比如http://localhost:3000和http://localhost:8080在某种程度上可以共享同一个 cookie只要 domain 相同。这既是便利也是潜在风险来源所以现代浏览器对手动设置 cookie 的 domain 有了更严格的约束——前端 JS 不能设置一个当前域名的父级域名对应的 cookie。例如你的页面在www.example.com你无法通过 JS 把 cookie 设置成domainexample.com因为这会扩大 cookie 的影响范围浏览器直接拒绝。服务端通过 Set-Cookie 响应头设置时限制相对宽松一些可以设置为当前域名的父级域名。这也解释了为什么 HTTP-only 的跨子域 cookie 通常是由服务端而不是前端处理的。另外第三方 cookie 正在被浏览器逐步淘汰。所谓第三方 cookie就是在访问 A 站时B 站域名下的 cookie。典型场景是广告追踪。Safari 的 ITPIntelligent Tracking Prevention机制早就对第三方 cookie 做了严格限制Chrome 也在推进相关策略。对于做前端的开发者这意味着不要依赖第三方 cookie 做核心业务逻辑。如果你在多个站点之间需要共享用户状态更靠谱的方案是自己实现一套基于 token 的身份认证而不是依赖浏览器对第三方 cookie 的放行。还有个容易被忽略的坑cookie 的 domain 和 path 匹配跟端口没有关系但一旦设置了端口相关的内容比如 localhost 携带端口号就可能导致奇怪的兼容问题。开发环境建议让后端把 cookie 的 domain 设置为localhost而不是127.0.0.1两者在部分浏览器规则下有区别。结尾cookie 本身是个几十年的老技术了但偏偏是这个“老”字让它积累了不少反直觉的细节。我在项目里踩过的坑从 path 丢失到中文乱码从删除不掉到 SameSite 拦截每一个都不是大问题但每一个都能让人排查一个下午。这篇博客里的封装和排查思路是我在多个真实项目中反复验证过的直接拿去用没问题。最后一个小提醒涉及登录态和用户身份的 cookie一定优先交给服务端用 HttpOnly Secure SameSite 来控制前端只管挖坑埋坑安全边界这堵墙得由服务端来砌。
