WinddSnow

JavaScript-JSON-and-Web-Storage

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

第56课:JSON 与数据存储——JSON.parse/stringifylocalStoragesessionStorage

在 Web 应用中,数据的持久化和交换离不开两种关键能力:序列化与反序列化(将数据结构转为文本,再从文本还原)和浏览器端存储(在用户设备上保存数据)。JSON(JavaScript Object Notation)是 Web 生态中最通用的数据交换格式,而 localStoragesessionStorage 是浏览器提供的键值对存储 API。本节课将深入讲解 JSON 序列化的精细控制(包括 replacerreviver 回调)、两种 Web Storage 的 API 差异与容量限制、以及实际开发中的安全与性能注意事项。


1. JSON:JavaScript 对象标记法

JSON 是独立于语言的纯文本数据格式,基于 JavaScript 对象字面量语法,但比 JS 对象更严格。它仅支持六种数据类型:字符串、数字、布尔值、null、数组、对象。不支持 undefined、函数、Symbol、BigInt、InfinityNaN

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.jsontsconfig.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-InfinityNaN 转为 null
布尔值 truefalse
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 接口 getItemsetItemremoveItemclearkey 完全相同。
存储类型 仅支持字符串。非字符串数据需手动序列化。 同左。

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 读写,但这种形式会与 lengthkeygetItem 等内置属性名冲突。始终使用 getItemsetItem 方法

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 对象包含 keyoldValuenewValueurl(触发变更的页面 URL)、storageArealocalStoragesessionStorage)。**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 本身不支持数据过期机制。一个常见的做法是存储一个包含 valueexpiry 时间戳的对象。

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. (单选) localStoragesessionStorage 的主要区别是?
    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.stringifyJSON.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 编程任务参考答案(提示词示例)