WinddSnow

JavaScript-JSON-and-Web-Storage

字数统计: 3.8k阅读时长: 16 min
2026/07/30

第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
2
3
4
5
6
7
8
9
10
{
"name": "Alice",
"age": 25,
"isAdmin": false,
"hobbies": ["reading", "coding"],
"address": {
"city": "Beijing",
"zip": null
}
}

JSON 在 Web 开发中的三大核心用途:

  • 客户端与服务端数据交换(取代 XML 成为 RESTful API 的通用格式)。
  • 配置文件(如 package.json、tsconfig.json)。
  • 浏览器端存储结构化数据(将对象序列化为字符串存入 localStorage)。

1.1 JSON.stringify:JavaScript 值 → JSON 字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const user = {
name: 'Alice',
age: 25,
isAdmin: false,
hobbies: ['reading', 'coding'],
greet: function() { return 'Hi'; }, // 函数 → 被忽略
created: new Date(), // Date → 转为 ISO 字符串
undef: undefined, // undefined → 被忽略
[Symbol('id')]: 123 // Symbol 键 → 被忽略
};

const json = JSON.stringify(user);
console.log(json);
// {"name":"Alice","age":25,"isAdmin":false,"hobbies":["reading","coding"],"created":"2026-04-15T10:30:00.000Z"}

序列化规则总结:

输入类型 序列化行为
对象 仅序列化可枚举的自有属性,忽略 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
2
3
4
5
const user = { name: 'Alice', password: 'secret123', age: 25, email: 'alice@example.com' };

// 仅序列化 name 和 age,过滤掉敏感数据
const safe = JSON.stringify(user, ['name', 'age']);
console.log(safe); // {"name":"Alice","age":25}

函数形式——逐键处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = { a: 1, b: undefined, c: 'hello', d: /regex/ };

const json = JSON.stringify(obj, (key, value) => {
// 过滤掉 undefined 和正则表达式
if (value === undefined || value instanceof RegExp) {
return undefined; // 返回 undefined 表示移除该属性
}
// 将所有字符串值转为大写
if (typeof value === 'string') {
return value.toUpperCase();
}
return value;
});
console.log(json); // {"a":1,"c":"HELLO"}

replacer 函数签名:(key, value) => newValue。首次调用时 key 为空字符串,value 为整个被序列化的对象。返回 undefined 会从结果中移除该属性。

1.3 space 参数:美化输出

第三个参数 space 控制缩进格式。可为数字(空格数)或字符串(如 '\t'、'--')。

1
2
3
4
5
6
const data = { name: 'Alice', age: 25 };
console.log(JSON.stringify(data, null, 2));
// {
// "name": "Alice",
// "age": 25
// }

1.4 toJSON 方法:对象自定义序列化

如果被序列化的对象拥有 toJSON() 方法,JSON.stringify 将调用该方法并使用其返回值作为序列化结果。这是 Date 对象能被自动转为 ISO 字符串的原因(Date.prototype.toJSON 存在)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const product = {
name: 'Widget',
price: 99,
_internalId: 'xyz-123',
toJSON() {
// 隐藏内部属性,自定义输出格式
return {
name: this.name,
price: `$${this.price.toFixed(2)}`
};
}
};

console.log(JSON.stringify(product)); // {"name":"Widget","price":"$99.00"}

1.5 JSON.parse:JSON 字符串 → JavaScript 值

JSON.parse(jsonString, reviver?) 将合法的 JSON 字符串解析为 JavaScript 值。

1
2
3
4
const json = '{"name":"Alice","age":25,"birth":"2001-04-15T00:00:00.000Z"}';

const obj = JSON.parse(json);
console.log(obj.birth); // '2001-04-15T00:00:00.000Z' —— 字符串,而非 Date 对象

reviver 参数:与 replacer 对称,用于在解析过程中转换每个键值对。常用于将日期字符串还原为 Date 对象。

1
2
3
4
5
6
7
8
9
10
const obj = JSON.parse(json, (key, value) => {
// 检测是否为 ISO 日期字符串
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(value)) {
return new Date(value);
}
return value;
});

console.log(obj.birth instanceof Date); // true
console.log(obj.birth.getFullYear()); // 2001

reviver 函数在从最内层到最外层遍历解析后的对象时调用。返回值将替代当前值,若返回 undefined 则删除该属性。

解析失败的异常处理:无效 JSON 会抛出 SyntaxError。应始终配合 try-catch 使用:

1
2
3
4
5
6
7
8
function safeParse(str, fallback = null) {
try {
return JSON.parse(str);
} catch (e) {
console.warn('JSON 解析失败:', e.message);
return fallback;
}
}

2. Web Storage:浏览器端键值存储

Web Storage API 提供了两种在浏览器中存储字符串数据的方式:localStorage(持久化)和 sessionStorage(会话级)。两者共享相同的 API 接口。

特性 localStorage sessionStorage
生命周期 永久存储,除非主动删除或用户清除浏览器数据。 标签页关闭即销毁(会话结束)。
作用域 同源(协议 + 域名 + 端口)的所有标签页共享。 仅限当前标签页。同源的其他标签页有独立存储。
容量 约 5MB(各浏览器略有差异)。 同左(约 5MB)。
API 接口 getItem、setItem、removeItem、clear、key。 完全相同。
存储类型 仅支持字符串。非字符串数据需手动序列化。 同左。

2.1 基本 API 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 存储
localStorage.setItem('theme', 'dark');
localStorage.setItem('user', JSON.stringify({ name: 'Alice', role: 'admin' }));

// 读取
const theme = localStorage.getItem('theme'); // 'dark'
const user = JSON.parse(localStorage.getItem('user')); // { name: 'Alice', role: 'admin' }

// 读取不存在的键
console.log(localStorage.getItem('nonexistent')); // null

// 删除单个键
localStorage.removeItem('theme');

// 清空所有键
localStorage.clear();

简写属性访问(不推荐):可以直接使用 localStorage.myKey = 'value' 和 localStorage.myKey 读写,但这种形式会与 length、key、getItem 等内置属性名冲突。始终使用 getItem 和 setItem 方法。

2.2 遍历所有存储项

1
2
3
4
5
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(`${key}: ${value}`);
}

2.3 存储事件:跨标签页通信

当其他同源标签页修改 localStorage 时,当前页面会触发 storage 事件。这可用于实现跨标签页的数据同步(注意:同一标签页内的修改不会触发该事件)。

1
2
3
4
5
6
window.addEventListener('storage', (event) => {
console.log('存储键:', event.key);
console.log('旧值:', event.oldValue);
console.log('新值:', event.newValue);
console.log('来源 URL:', event.url);
});

event 对象包含 key、oldValue、newValue、url(触发变更的页面 URL)、storageArea(localStorage 或 sessionStorage)。**sessionStorage 不会触发 storage 事件**,因为它仅在当前标签页中有效。

2.4 容量限制与超出处理

当存储数据接近或超过浏览器限额(约 5MB)时,setItem 会抛出 QuotaExceededError(在 Chrome 中为 DOMException)。应在写入前或捕获异常后处理。

1
2
3
4
5
6
7
8
9
10
11
12
function safeSetItem(key, value) {
try {
localStorage.setItem(key, value);
} catch (e) {
if (e.name === 'QuotaExceededError' || e.code === 22) {
console.warn('存储空间已满,尝试清理旧数据');
// 清理策略:移除最旧的条目
} else {
throw e;
}
}
}

3. 实战模式与最佳实践

3.1 带过期时间的缓存

localStorage 本身不支持数据过期机制。一个常见的做法是存储一个包含 value 和 expiry 时间戳的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function setWithExpiry(key, value, ttlMs) {
const now = Date.now();
const item = {
value,
expiry: now + ttlMs
};
localStorage.setItem(key, JSON.stringify(item));
}

function getWithExpiry(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) return null;

const item = JSON.parse(itemStr);
if (Date.now() > item.expiry) {
localStorage.removeItem(key); // 过期清理
return null;
}
return item.value;
}

3.2 封装类型安全的存储工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const storage = {
get(key, fallback = null) {
try {
const raw = localStorage.getItem(key);
return raw !== null ? JSON.parse(raw) : fallback;
} catch {
return fallback;
}
},
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch {
return false; // 存储失败(如配额溢出)
}
},
remove(key) {
localStorage.removeItem(key);
}
};

// 使用
storage.set('preferences', { theme: 'dark', fontSize: 14 });
const prefs = storage.get('preferences', { theme: 'light', fontSize: 16 });

3.3 安全注意

  • 永远不要存储敏感信息(密码、Token、个人信息明文)在 Web Storage 中。它是同步 API,易受 XSS 攻击——任何注入页面的恶意脚本都可以读取 localStorage 的全部内容。对于认证 Token,优先使用 httpOnly 的 Cookie。
  • 存储体积影响性能:localStorage 是同步 API,读写大量数据会阻塞主线程。单个键的值建议不超过 100KB。
  • 无痕/隐私模式:在某些浏览器的隐私模式下,localStorage 可能不可用或容量极小(甚至每个键只能存储少量数据)。使用前应做特性检测。
1
2
3
4
5
6
7
8
9
10
11
12
function isStorageAvailable(type) {
try {
const storage = window[type];
const testKey = '__storage_test__';
storage.setItem(testKey, 'test');
storage.removeItem(testKey);
return true;
} catch {
return false;
}
}
console.log(isStorageAvailable('localStorage')); // true 或 false

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) JSON.stringify 遇到 undefined 值时,在对象属性中会如何处理?
    A. 转为字符串 "undefined"
    B. 转为 null
    C. 忽略该属性(不包含在 JSON 结果中)
    D. 抛出 TypeError

  2. (单选) localStorage 和 sessionStorage 的主要区别是?
    A. localStorage 容量更大。
    B. sessionStorage 在标签页关闭后数据被清除。
    C. sessionStorage 仅支持字符串,localStorage 支持对象。
    D. localStorage 是异步的,sessionStorage 是同步的。

  3. (填空) 在 JSON.parse 中,要对解析后的每个键值对进行转换,应使用第二个参数 ______。

  4. (多选) 以下哪些类型的值无法被 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. 更安全的替代方案:

  • httpOnly Cookie:设置 httpOnly 标志的 Cookie 无法通过 JavaScript 访问(document.cookie 不可读取),仅随 HTTP 请求自动发送。配合 Secure(仅 HTTPS)和 SameSite=Strict 标志,可有效防御 XSS 和 CSRF 攻击。
  • Secure + SameSite Cookie:确保凭证仅在加密连接中传输,且仅在同站请求中发送。
  • BFF(Backend For Frontend)模式:Token 存储在服务端,通过 Session Cookie 将浏览器与后端会话关联,前端完全不接触凭证。
  • Web Worker 中的隔离存储:将敏感 Token 存储在 Web Worker 的闭包中,通过 postMessage 提供有限的接口,使主线程代码无法直接访问。

3. 如果确实需要前端存储(如离线应用),遵循最小权限原则:

  • 仅存储短期有效的访问令牌(Access Token),将长期有效的刷新令牌(Refresh Token)保存在 httpOnly Cookie 中。
  • 对存储到 localStorage 的数据进行加密(但加密密钥同样暴露在前端,仅增加攻击复杂度而非根除风险)。
  • 实施严格的 CSP(Content Security Policy)限制脚本来源,降低 XSS 攻击的风险。

课后练习答案

一、概念自测答案

  1. C

    • 解析:JSON.stringify 在对象属性中遇到 undefined、函数、Symbol 时会忽略该属性(在数组中会转为 null)。
  2. B

    • 解析:sessionStorage 在标签页关闭后被清除,localStorage 永久保留。两者都是同步 API,都仅支持字符串存储,默认容量相似(约 5MB)。
  3. reviver

    • 解析:JSON.parse(text, reviver) 的第二个参数是 reviver 函数,用于转换解析后的每个键值对。
  4. A、B、C

    • 解析:函数和 Symbol 会被忽略(或转 null),BigInt 直接抛出 TypeError。Date 对象会正确序列化为 ISO 字符串,因为它有 toJSON 方法。

二、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 注释。
  • 提供使用示例注释。直接输出完整代码。”
CATALOG
  1. 1. 第56课:JSON 与数据存储——JSON.parse/stringify、localStorage、sessionStorage
    1. 1.1. 1. JSON:JavaScript 对象标记法
      1. 1.1.1. 1.1 JSON.stringify:JavaScript 值 → JSON 字符串
      2. 1.1.2. 1.2 replacer 参数:自定义序列化行为
      3. 1.1.3. 1.3 space 参数:美化输出
      4. 1.1.4. 1.4 toJSON 方法:对象自定义序列化
      5. 1.1.5. 1.5 JSON.parse:JSON 字符串 → JavaScript 值
    2. 1.2. 2. Web Storage:浏览器端键值存储
      1. 1.2.1. 2.1 基本 API 操作
      2. 1.2.2. 2.2 遍历所有存储项
      3. 1.2.3. 2.3 存储事件:跨标签页通信
      4. 1.2.4. 2.4 容量限制与超出处理
    3. 1.3. 3. 实战模式与最佳实践
      1. 1.3.1. 3.1 带过期时间的缓存
      2. 1.3.2. 3.2 封装类型安全的存储工具
      3. 1.3.3. 3.3 安全注意
    4. 1.4. 课后练习
      1. 1.4.1. 一、概念自测(选择题 / 填空题)
      2. 1.4.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.4.3. 三、面试真题与参考答案
    5. 1.5. 课后练习答案
      1. 1.5.1. 一、概念自测答案
      2. 1.5.2. 二、AI 编程任务参考答案(提示词示例)