第56课:JSON 与数据存储——JSON.parse/stringify、localStorage、sessionStorage
在 Web 应用中,数据的持久化和交换离不开两种关键能力:序列化与反序列化(将数据结构转为文本,再从文本还原)和浏览器端存储(在用户设备上保存数据)。JSON(JavaScript Object Notation)是 Web 生态中最通用的数据交换格式,而 localStorage 和 sessionStorage 是浏览器提供的键值对存储 API。本节课将深入讲解 JSON 序列化的精细控制(包括 replacer 和 reviver 回调)、两种 Web Storage 的 API 差异与容量限制、以及实际开发中的安全与性能注意事项。
1. JSON:JavaScript 对象标记法
JSON 是独立于语言的纯文本数据格式,基于 JavaScript 对象字面量语法,但比 JS 对象更严格。它仅支持六种数据类型:字符串、数字、布尔值、null、数组、对象。不支持 undefined、函数、Symbol、BigInt、Infinity、NaN。
1 | { |
JSON 在 Web 开发中的三大核心用途:
- 客户端与服务端数据交换(取代 XML 成为 RESTful API 的通用格式)。
- 配置文件(如
package.json、tsconfig.json)。 - 浏览器端存储结构化数据(将对象序列化为字符串存入
localStorage)。
1.1 JSON.stringify:JavaScript 值 → JSON 字符串
1 | const user = { |
序列化规则总结:
| 输入类型 | 序列化行为 |
|---|---|
| 对象 | 仅序列化可枚举的自有属性,忽略 Symbol 键和不可枚举属性。 |
| 数组 | 按顺序序列化元素,undefined 和函数等无效值转为 null(在数组中)。 |
| 字符串 | 双引号包裹,特殊字符转义(" → \"、\ → \\、换行 → \n)。 |
| 数字 | 直接输出,但 Infinity、-Infinity、NaN 转为 null。 |
| 布尔值 | true 或 false。 |
null |
null。 |
函数、undefined、Symbol |
在对象属性中被忽略,在数组元素中转为 null。 |
Date |
调用 toISOString() 转为 ISO 格式字符串。 |
BigInt |
**抛出 TypeError**——BigInt 不能直接序列化。 |
| 循环引用 | **抛出 TypeError**——无法序列化循环引用结构。 |
1.2 replacer 参数:自定义序列化行为
JSON.stringify 的第二个参数 replacer 可以是一个过滤数组(仅序列化指定键)或一个转换函数(对每个键值对进行处理)。
数组形式——白名单过滤:
1 | const user = { name: 'Alice', password: 'secret123', age: 25, email: 'alice@example.com' }; |
函数形式——逐键处理:
1 | const obj = { a: 1, b: undefined, c: 'hello', d: /regex/ }; |
replacer 函数签名:(key, value) => newValue。首次调用时 key 为空字符串,value 为整个被序列化的对象。返回 undefined 会从结果中移除该属性。
1.3 space 参数:美化输出
第三个参数 space 控制缩进格式。可为数字(空格数)或字符串(如 '\t'、'--')。
1 | const data = { name: 'Alice', age: 25 }; |
1.4 toJSON 方法:对象自定义序列化
如果被序列化的对象拥有 toJSON() 方法,JSON.stringify 将调用该方法并使用其返回值作为序列化结果。这是 Date 对象能被自动转为 ISO 字符串的原因(Date.prototype.toJSON 存在)。
1 | const product = { |
1.5 JSON.parse:JSON 字符串 → JavaScript 值
JSON.parse(jsonString, reviver?) 将合法的 JSON 字符串解析为 JavaScript 值。
1 | const json = '{"name":"Alice","age":25,"birth":"2001-04-15T00:00:00.000Z"}'; |
reviver 参数:与 replacer 对称,用于在解析过程中转换每个键值对。常用于将日期字符串还原为 Date 对象。
1 | const obj = JSON.parse(json, (key, value) => { |
reviver 函数在从最内层到最外层遍历解析后的对象时调用。返回值将替代当前值,若返回 undefined 则删除该属性。
解析失败的异常处理:无效 JSON 会抛出 SyntaxError。应始终配合 try-catch 使用:
1 | function safeParse(str, fallback = null) { |
2. Web Storage:浏览器端键值存储
Web Storage API 提供了两种在浏览器中存储字符串数据的方式:localStorage(持久化)和 sessionStorage(会话级)。两者共享相同的 API 接口。
| 特性 | localStorage |
sessionStorage |
|---|---|---|
| 生命周期 | 永久存储,除非主动删除或用户清除浏览器数据。 | 标签页关闭即销毁(会话结束)。 |
| 作用域 | 同源(协议 + 域名 + 端口)的所有标签页共享。 | 仅限当前标签页。同源的其他标签页有独立存储。 |
| 容量 | 约 5MB(各浏览器略有差异)。 | 同左(约 5MB)。 |
| API 接口 | getItem、setItem、removeItem、clear、key。 |
完全相同。 |
| 存储类型 | 仅支持字符串。非字符串数据需手动序列化。 | 同左。 |
2.1 基本 API 操作
1 | // 存储 |
简写属性访问(不推荐):可以直接使用 localStorage.myKey = 'value' 和 localStorage.myKey 读写,但这种形式会与 length、key、getItem 等内置属性名冲突。始终使用 getItem 和 setItem 方法。
2.2 遍历所有存储项
1 | for (let i = 0; i < localStorage.length; i++) { |
2.3 存储事件:跨标签页通信
当其他同源标签页修改 localStorage 时,当前页面会触发 storage 事件。这可用于实现跨标签页的数据同步(注意:同一标签页内的修改不会触发该事件)。
1 | window.addEventListener('storage', (event) => { |
event 对象包含 key、oldValue、newValue、url(触发变更的页面 URL)、storageArea(localStorage 或 sessionStorage)。**sessionStorage 不会触发 storage 事件**,因为它仅在当前标签页中有效。
2.4 容量限制与超出处理
当存储数据接近或超过浏览器限额(约 5MB)时,setItem 会抛出 QuotaExceededError(在 Chrome 中为 DOMException)。应在写入前或捕获异常后处理。
1 | function safeSetItem(key, value) { |
3. 实战模式与最佳实践
3.1 带过期时间的缓存
localStorage 本身不支持数据过期机制。一个常见的做法是存储一个包含 value 和 expiry 时间戳的对象。
1 | function setWithExpiry(key, value, ttlMs) { |
3.2 封装类型安全的存储工具
1 | const storage = { |
3.3 安全注意
- 永远不要存储敏感信息(密码、Token、个人信息明文)在 Web Storage 中。它是同步 API,易受 XSS 攻击——任何注入页面的恶意脚本都可以读取
localStorage的全部内容。对于认证 Token,优先使用httpOnly的 Cookie。 - 存储体积影响性能:
localStorage是同步 API,读写大量数据会阻塞主线程。单个键的值建议不超过 100KB。 - 无痕/隐私模式:在某些浏览器的隐私模式下,
localStorage可能不可用或容量极小(甚至每个键只能存储少量数据)。使用前应做特性检测。
1 | function isStorageAvailable(type) { |
课后练习
一、概念自测(选择题 / 填空题)
(单选)
JSON.stringify遇到undefined值时,在对象属性中会如何处理?
A. 转为字符串"undefined"
B. 转为null
C. 忽略该属性(不包含在 JSON 结果中)
D. 抛出 TypeError(单选)
localStorage和sessionStorage的主要区别是?
A.localStorage容量更大。
B.sessionStorage在标签页关闭后数据被清除。
C.sessionStorage仅支持字符串,localStorage支持对象。
D.localStorage是异步的,sessionStorage是同步的。(填空) 在
JSON.parse中,要对解析后的每个键值对进行转换,应使用第二个参数______。(多选) 以下哪些类型的值无法被
JSON.stringify正确序列化(会被忽略、转成null或抛出错误)?
A.function
B.Symbol
C.BigInt
D.Date
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要编写一个通用的浏览器缓存工具模块,提供以下功能:
cache.set(key, value, ttlSeconds?):存储值,如果提供了ttlSeconds,则设置过期时间(超过后get返回null)。内部使用localStorage。cache.get(key):读取值,若已过期则自动删除并返回null。若不存在返回null。cache.remove(key):删除指定键。cache.clear():清空所有该模块管理的缓存(可通过前缀识别,如所有键以cache:开头)。- 使用
JSON.stringify和JSON.parse进行序列化/反序列化,使用try-catch处理解析异常和配额溢出。 - 使用
const声明所有变量,添加 JSDoc 注释。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定缓存过期机制、前缀过滤、错误处理策略。
三、面试真题与参考答案
题目(京东前端面试题):
请解释为什么不应该在
localStorage中存储敏感信息(如 JWT Token)?如果必须在前端存储会话凭证,有什么更安全的替代方案?
参考答案:
1. localStorage 存储敏感信息的风险:
- XSS 攻击:
localStorage可通过 JavaScript 直接读取。一旦网站存在 XSS 漏洞,攻击者可以注入脚本读取所有localStorage数据并发送到外部服务器。这是一种同步、无阻碍的访问——恶意脚本执行后即可获取所有值。 - 无过期机制:
localStorage中的数据是永久的,除非手动清除。如果用户忘记注销或凭证泄漏,攻击者可在较长时间内利用窃取的 Token。 - 无加密:存储的值是明文,任何有权限访问浏览器文件系统的程序(浏览器扩展、恶意软件)都可以直接读取。
- 作用域为同源:同一源下的所有脚本(包括被注入的第三方广告脚本)均可访问全部数据。
2. 更安全的替代方案:
httpOnlyCookie:设置httpOnly标志的 Cookie 无法通过 JavaScript 访问(document.cookie不可读取),仅随 HTTP 请求自动发送。配合Secure(仅 HTTPS)和SameSite=Strict标志,可有效防御 XSS 和 CSRF 攻击。Secure+SameSiteCookie:确保凭证仅在加密连接中传输,且仅在同站请求中发送。- BFF(Backend For Frontend)模式:Token 存储在服务端,通过 Session Cookie 将浏览器与后端会话关联,前端完全不接触凭证。
- Web Worker 中的隔离存储:将敏感 Token 存储在 Web Worker 的闭包中,通过
postMessage提供有限的接口,使主线程代码无法直接访问。
3. 如果确实需要前端存储(如离线应用),遵循最小权限原则:
- 仅存储短期有效的访问令牌(Access Token),将长期有效的刷新令牌(Refresh Token)保存在
httpOnlyCookie 中。 - 对存储到
localStorage的数据进行加密(但加密密钥同样暴露在前端,仅增加攻击复杂度而非根除风险)。 - 实施严格的 CSP(Content Security Policy)限制脚本来源,降低 XSS 攻击的风险。
课后练习答案
一、概念自测答案
C
- 解析:
JSON.stringify在对象属性中遇到undefined、函数、Symbol 时会忽略该属性(在数组中会转为null)。
- 解析:
B
- 解析:
sessionStorage在标签页关闭后被清除,localStorage永久保留。两者都是同步 API,都仅支持字符串存储,默认容量相似(约 5MB)。
- 解析:
reviver- 解析:
JSON.parse(text, reviver)的第二个参数是 reviver 函数,用于转换解析后的每个键值对。
- 解析:
A、B、C
- 解析:函数和 Symbol 会被忽略(或转
null),BigInt 直接抛出TypeError。Date 对象会正确序列化为 ISO 字符串,因为它有toJSON方法。
- 解析:函数和 Symbol 会被忽略(或转
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请编写一个基于localStorage的缓存工具模块,支持过期时间。要求:
- 导出对象
cache,包含方法set(key, value, ttlSeconds?)、get(key)、remove(key)、clear()。set方法:用JSON.stringify序列化{ value, expiry: ttlSeconds ? Date.now() + ttlSeconds * 1000 : null },存入cache:${key}的键中。捕获QuotaExceededError并返回false(成功返回true)。get方法:读取并JSON.parse,若过期则调用remove并返回null。捕获解析异常返回null。remove删除对应键。clear遍历所有cache:前缀的键并删除(用localStorage.key()循环)。- 使用
const声明所有变量,添加 JSDoc 注释。- 提供使用示例注释。直接输出完整代码。”