JSON 不只是格式化:前端开发者必知的数据处理技巧
别只会 JSON.parse 和 JSON.stringify 了。这 6 个技巧能让你的 JSON 处理代码更健壮、更安全、更高效。
JSON 是前端开发中最常打交道的数据格式。大多数人对它的认知停留在「格式化一下看看结构」或者「调接口解析一下」。但 JSON 相关的 API 其实隐藏了不少强大的功能,用好了可以省掉很多第三方库。
这篇文章从 6 个维度深入讲解 JSON 在前端的进阶用法:自定义序列化、智能反序列化、深拷贝方案对比、数据校验、性能优化和安全防护。每个部分都有真实业务场景和可运行的代码示例。
1. JSON.stringify 的隐藏参数
绝大多数人用 JSON.stringify 都只传一个参数。但它其实接受三个参数:JSON.stringify(value, replacer, space)。后两个参数能解锁很多实用功能。
space 参数:美化输出
第三个参数 space 控制缩进。传数字表示空格数,传字符串表示用该字符串作为缩进符。
const user = { name: 'Tom', age: 25, skills: ['JS', 'TS'] }
// 默认:所有内容挤在一行
JSON.stringify(user)
// '{"name":"Tom","age":25,"skills":["JS","TS"]}'
// 2 空格缩进:可读性大幅提升
JSON.stringify(user, null, 2)
// {
// "name": "Tom",
// "age": 25,
// "skills": [
// "JS",
// "TS"
// ]
// }
// 用 tab 缩进
JSON.stringify(user, null, '\t')
// 甚至可以用 emoji(调试时好玩)
JSON.stringify(user, null, '🔹')
replacer 参数:精确控制序列化
第二个参数 replacer 有两种形式:数组(白名单)和函数(自定义转换)。
// 方式一:数组 —— 只保留指定字段(白名单过滤)
const user = { name: 'Tom', password: '123456', email: 'tom@test.com', role: 'admin' }
// 在日志中打印用户信息时,过滤掉敏感字段
const safeLog = JSON.stringify(user, ['name', 'email', 'role'], 2)
// {
// "name": "Tom",
// "email": "tom@test.com",
// "role": "admin"
// }
// password 被自动过滤掉了!
// 方式二:函数 —— 自定义每个值的序列化逻辑
const data = {
name: 'project',
created: new Date('2026-01-01'),
config: { debug: true, version: 3 },
secret: 'should-be-hidden'
}
const result = JSON.stringify(data, (key, value) => {
// 把 Date 对象转成 ISO 字符串
if (value instanceof Date) return value.toISOString()
// 过滤掉包含 secret 的字段
if (key.toLowerCase().includes('secret')) return undefined
// 其他值原样返回
return value
}, 2)
// {
// "name": "project",
// "created": "2026-01-01T00:00:00.000Z",
// "config": { "debug": true, "version": 3 }
// }
// secret 字段消失了,Date 被转成了字符串
实战场景:toJSON 方法
如果一个对象定义了 toJSON() 方法,JSON.stringify 会优先调用它。这在封装类时非常有用。比如你可以让一个 User 类在序列化时自动隐藏密码字段,而不需要每次手动写 replacer。Date 对象能被正确序列化,就是因为它内置了 toJSON() 方法。
2. JSON.parse 的 reviver 妙用
JSON.parse 也有第二个参数——reviver(复活器)。它在反序列化时被调用,可以对每个值做自定义转换。这是解决「JSON 不支持的类型」问题的最佳方案。
日期字符串自动转 Date 对象
JSON 没有日期类型。API 返回的日期通常是 ISO 字符串,拿到后还得手动转 new Date()。用 reviver 可以自动化这个过程。
// ISO 日期字符串的正则
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/
function dateReviver(key: string, value: unknown) {
if (typeof value === 'string' && ISO_DATE_RE.test(value)) {
return new Date(value)
}
return value
}
// 使用
const json = '{"name":"Tom","createdAt":"2026-03-18T08:00:00.000Z","age":25}'
const parsed = JSON.parse(json, dateReviver)
console.log(parsed.createdAt instanceof Date) // true
console.log(parsed.createdAt.getFullYear()) // 2026
console.log(typeof parsed.age) // 'number'(不受影响)
BigInt 的序列化和反序列化
JSON 标准不支持 BigInt,直接 JSON.stringify 一个包含 BigInt 的对象会直接报错。但后端(如 Java 的 Long 类型)经常返回超过 Number.MAX_SAFE_INTEGER 的大整数。这里有一套完整的解决方案:
// ❌ 直接序列化 BigInt 会报错
const data = { id: 9007199254740993n, name: 'Tom' }
JSON.stringify(data) // TypeError: Do not know how to serialize a BigInt
// ✅ 方案:replacer + reviver 配合
// 序列化:把 BigInt 转成带标记的字符串
function bigintReplacer(key: string, value: unknown) {
if (typeof value === 'bigint') {
return { __type: 'bigint', value: value.toString() }
}
return value
}
// 反序列化:识别标记并还原
function bigintReviver(key: string, value: unknown) {
if (value && typeof value === 'object' && (value as any).__type === 'bigint') {
return BigInt((value as any).value)
}
return value
}
// 完整使用示例
const original = { orderId: 9007199254740993n, amount: 99.5 }
const jsonStr = JSON.stringify(original, bigintReplacer)
// '{"orderId":{"__type":"bigint","value":"9007199254740993"},"amount":99.5}'
const restored = JSON.parse(jsonStr, bigintReviver)
console.log(restored.orderId) // 9007199254740993n
console.log(typeof restored.orderId) // 'bigint'
console.log(restored.amount) // 99.5
原理:reviver 的执行顺序
reviver 是自底向上执行的——先处理最深层的叶子节点,最后处理根对象。这意味着当你处理一个嵌套对象时,内部的值已经被 reviver 转换过了。最后一次调用的 key 是空字符串 "",对应的 value 是整个解析后的根对象。
3. 深拷贝:structuredClone vs JSON 序列化
JSON.parse(JSON.stringify(obj)) 一直是前端「穷人版深拷贝」的经典写法。但 2022 年浏览器引入了 structuredClone。这两种方案各有优缺点,选择取决于你的具体场景。
JSON 序列化方案
const original = {
name: 'Tom',
address: { city: 'Beijing', zip: '100000' },
tags: ['dev', 'frontend']
}
// JSON 深拷贝
const cloned = JSON.parse(JSON.stringify(original))
cloned.address.city = 'Shanghai'
console.log(original.address.city) // 'Beijing' —— 确实是深拷贝
// ⚠️ 但是有很多边界情况
const problematic = {
date: new Date(), // 变成字符串
regex: /hello/gi, // 变成空对象 {}
func: () => 'hi', // 直接丢失(undefined)
undef: undefined, // 直接丢失
nan: NaN, // 变成 null
infinity: Infinity, // 变成 null
map: new Map([['a', 1]]), // 变成空对象 {}
set: new Set([1, 2, 3]), // 变成空对象 {}
}
const jsonClone = JSON.parse(JSON.stringify(problematic))
console.log(jsonClone.date) // "2026-03-18T..." (字符串,不是 Date)
console.log(jsonClone.regex) // {}
console.log(jsonClone.func) // undefined (字段消失了)
console.log(jsonClone.nan) // null
console.log(jsonClone.map) // {}
structuredClone 方案
const original = {
date: new Date(),
map: new Map([['a', 1]]),
set: new Set([1, 2, 3]),
buffer: new ArrayBuffer(8),
regex: /hello/gi,
nested: { deep: { value: 42 } }
}
const cloned = structuredClone(original)
console.log(cloned.date instanceof Date) // true ✅
console.log(cloned.map instanceof Map) // true ✅
console.log(cloned.set instanceof Set) // true ✅
console.log(cloned.regex instanceof RegExp) // true ✅
// ⚠️ structuredClone 也有限制
const cantClone = {
func: () => 'hi', // ❌ 报错:函数不能被克隆
dom: document.createElement('div'), // ❌ 报错:DOM 节点不能被克隆
symbol: Symbol('test'), // ❌ 报错:Symbol 不能被克隆
}
// structuredClone(cantClone) // 抛出 DOMException
// 📊 两种方案的对比总结 // // 特性 JSON 方案 structuredClone // ────────────────────────────────────────────────────────── // Date ❌ 变成字符串 ✅ 保持 Date // Map / Set ❌ 变成空对象 ✅ 正确拷贝 // RegExp ❌ 变成空对象 ✅ 正确拷贝 // ArrayBuffer ❌ 不支持 ✅ 正确拷贝 // undefined ❌ 字段丢失 ✅ 保留 // NaN / Infinity ❌ 变成 null ✅ 保留 // 函数 ❌ 字段丢失 ❌ 抛出错误 // DOM 节点 ❌ 抛出错误 ❌ 抛出错误 // Symbol ❌ 字段丢失 ❌ 抛出错误 // 循环引用 ❌ 抛出错误 ✅ 正确处理 // 性能(大对象) 较慢 较快 // 浏览器兼容性 IE 9+ Chrome 98+
怎么选?
如果你的数据是纯 JSON 兼容的(只有字符串、数字、布尔值、数组和普通对象),两种方案都可以,JSON 方案甚至更通用。但如果数据包含 Date、Map、Set、循环引用等,优先使用 structuredClone。在 2026 年,structuredClone 的浏览器兼容性已经不是问题,建议作为默认选择。
4. JSON Schema:数据校验的标准方案
前端从 API 拿到的数据,结构真的可靠吗?后端改了字段名、少返回了一个字段、类型不匹配——这些问题在运行时才暴露,debug 起来非常痛苦。JSON Schema 是一套标准化的「JSON 结构描述语言」,可以在运行时校验数据是否符合预期结构。
基本语法
// 定义一个 User 的 JSON Schema
const userSchema = {
type: 'object',
required: ['id', 'name', 'email'],
properties: {
id: { type: 'number', minimum: 1 },
name: { type: 'string', minLength: 1, maxLength: 50 },
email: { type: 'string', format: 'email' },
age: { type: 'number', minimum: 0, maximum: 150 },
role: { type: 'string', enum: ['admin', 'user', 'guest'] },
tags: {
type: 'array',
items: { type: 'string' },
maxItems: 10
}
},
additionalProperties: false // 不允许多余字段
}
用 Ajv 在运行时校验
Ajv 是最流行的 JSON Schema 校验库,性能极好(编译 Schema 后缓存校验函数),包体积也合理。
import Ajv from 'ajv'
import addFormats from 'ajv-formats'
const ajv = new Ajv({ allErrors: true }) // allErrors: 收集所有错误而不是遇到第一个就停
addFormats(ajv) // 添加 email、uri 等格式支持
const validate = ajv.compile(userSchema)
// ✅ 合法数据
const validUser = { id: 1, name: 'Tom', email: 'tom@test.com', role: 'admin' }
console.log(validate(validUser)) // true
// ❌ 非法数据
const invalidUser = { id: -1, name: '', email: 'not-an-email', extra: 'field' }
console.log(validate(invalidUser)) // false
console.log(validate.errors)
// [
// { keyword: 'minimum', dataPath: '/id', message: 'must be >= 1' },
// { keyword: 'minLength', dataPath: '/name', message: 'must NOT have fewer than 1 characters' },
// { keyword: 'format', dataPath: '/email', message: 'must match format "email"' },
// { keyword: 'additionalProperties', message: 'must NOT have additional properties' }
// ]
// 实战:封装 API 响应校验中间件
function createApiValidator<T>(schema: object) {
const ajv = new Ajv({ allErrors: true })
const validate = ajv.compile(schema)
return function validateResponse(data: unknown): T {
if (!validate(data)) {
const errors = validate.errors?.map(e =>
`${e.instancePath} ${e.message}`
).join('; ')
throw new Error(`API 响应格式错误: ${errors}`)
}
return data as T
}
}
// 使用
interface User { id: number; name: string; email: string }
const validateUser = createApiValidator<User>(userSchema)
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`)
const data = await res.json()
return validateUser(data) // 如果结构不对,这里会抛出清晰的错误
}
Schema 与 TypeScript 的关系
TypeScript 的类型检查只在编译时生效,运行时完全消失。JSON Schema 弥补了这个空白——它在运行时校验数据结构。两者配合使用是最佳实践:TypeScript 管编译时,JSON Schema 管运行时。你甚至可以用 json-schema-to-typescript 这个库从 Schema 自动生成 TypeScript 类型定义,保证两者始终同步。
5. 大型 JSON 处理:性能优化技巧
当 JSON 数据量达到几 MB 甚至几十 MB 时,JSON.parse 会阻塞主线程,导致页面卡顿。这在处理日志分析、数据可视化、大型配置文件等场景中很常见。以下是三种优化策略。
策略一:Web Worker 异步解析
把 JSON 解析放到 Web Worker 中,主线程完全不阻塞。
// json-worker.js
self.onmessage = function(e) {
try {
const data = JSON.parse(e.data)
self.postMessage({ success: true, data })
} catch (err) {
self.postMessage({ success: false, error: err.message })
}
}
// main.js —— 封装成 Promise
function parseJsonAsync(jsonString) {
return new Promise((resolve, reject) => {
const worker = new Worker('json-worker.js')
worker.onmessage = (e) => {
worker.terminate()
if (e.data.success) {
resolve(e.data.data)
} else {
reject(new Error(e.data.error))
}
}
worker.onerror = (err) => {
worker.terminate()
reject(err)
}
worker.postMessage(jsonString)
})
}
// 使用:主线程不会被阻塞
const hugeData = await fetch('/api/huge-dataset').then(r => r.text())
const parsed = await parseJsonAsync(hugeData)
console.log('解析完成,条目数:', parsed.length)
策略二:流式解析(Streaming JSON)
对于超大 JSON 数组,可以用流式解析逐条处理,避免一次性把整个 JSON 加载进内存。
// 使用 fetch + ReadableStream 逐块读取
async function streamJsonArray(url, onItem) {
const response = await fetch(url)
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
let depth = 0
let inString = false
let itemStart = -1
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
// 简化的逐字符解析(生产环境建议用 oboe.js 或 stream-json)
for (let i = 0; i < buffer.length; i++) {
const ch = buffer[i]
if (ch === '"' && buffer[i - 1] !== '\\') inString = !inString
if (inString) continue
if (ch === '{' || ch === '[') {
if (depth === 1 && ch === '{') itemStart = i
depth++
}
if (ch === '}' || ch === ']') {
depth--
if (depth === 1 && ch === '}' && itemStart !== -1) {
const item = JSON.parse(buffer.slice(itemStart, i + 1))
onItem(item) // 每解析出一条就回调
itemStart = -1
}
}
}
// 保留未处理完的部分
buffer = depth > 0 ? buffer : ''
}
}
// 使用:逐条处理 10 万条数据,内存占用极低
let count = 0
await streamJsonArray('/api/logs', (item) => {
count++
if (count % 10000 === 0) console.log(`已处理 ${count} 条`)
})
策略三:分页加载 + 虚拟滚动
// 分页加载 JSON 数据
class PaginatedJsonLoader {
constructor(url, pageSize = 100) {
this.url = url
this.pageSize = pageSize
this.cache = new Map()
}
async loadPage(page) {
if (this.cache.has(page)) return this.cache.get(page)
const res = await fetch(
`${this.url}?page=${page}&size=${this.pageSize}`
)
const data = await res.json()
this.cache.set(page, data)
// 只缓存最近 10 页,防止内存溢出
if (this.cache.size > 10) {
const oldest = this.cache.keys().next().value
this.cache.delete(oldest)
}
return data
}
// 配合虚拟滚动使用
async getItem(index) {
const page = Math.floor(index / this.pageSize)
const offset = index % this.pageSize
const data = await this.loadPage(page)
return data.items[offset]
}
}
// 使用:用户滚动时按需加载,初始加载几乎瞬间完成
const loader = new PaginatedJsonLoader('/api/products', 50)
const item = await loader.getItem(2500) // 自动加载第 50 页
性能基准参考
在 M1 MacBook 上测试,JSON.parse 处理 1MB 数据大约需要 5-10ms,10MB 约 50-100ms,50MB 就可能超过 500ms 导致明显卡顿。一般建议超过 5MB 的数据就考虑用 Web Worker,超过 50MB 考虑流式解析或分页加载。具体阈值取决于你的目标设备和性能要求。
6. JSON 安全陷阱
JSON 看起来人畜无害,但在处理不受信任的数据时,隐藏着好几个安全陷阱。如果你的代码处理用户输入或第三方 API 的数据,务必注意以下问题。
陷阱一:原型污染(Prototype Pollution)
攻击者可以通过 JSON 数据中的 __proto__ 或 constructor 字段,修改 JavaScript 对象的原型链,影响所有对象的行为。
// ❌ 危险:直接 merge 不受信任的 JSON 数据
function unsafeMerge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = target[key] || {}
unsafeMerge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
// 攻击者构造的恶意 JSON
const malicious = JSON.parse('{"__proto__":{"isAdmin":true}}')
const userConfig = {}
unsafeMerge(userConfig, malicious)
// 现在所有对象都被污染了!
const newObj = {}
console.log(newObj.isAdmin) // true 😱 所有新对象都变成了 admin
// ✅ 防御方案一:过滤危险属性
function safeMerge(target, source) {
const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype']
for (const key in source) {
if (DANGEROUS_KEYS.includes(key)) continue // 跳过危险 key
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = target[key] || {}
safeMerge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
// ✅ 防御方案二:使用 Object.create(null) 创建无原型对象
const safeObj = Object.create(null) // 没有 __proto__,无法被污染
Object.assign(safeObj, JSON.parse(untrustedJson))
// ✅ 防御方案三:用 Map 代替普通对象
const config = new Map(Object.entries(JSON.parse(untrustedJson)))
// Map 不受原型污染影响
陷阱二:XSS 注入
当 JSON 数据被直接嵌入 HTML 页面时,可能触发 XSS 攻击。这在服务端渲染(SSR)场景中尤其常见。
// ❌ 危险:直接把 JSON 嵌入到 <script> 标签中
const userData = { name: '</script><script>alert("XSS")</script>' }
// SSR 模板中这样写会被攻击:
// <script>window.__DATA__ = ${JSON.stringify(userData)}</script>
// 渲染结果:
// <script>window.__DATA__ = {"name":"</script><script>alert("XSS")</script>"}</script>
// </script> 提前关闭了标签,后面的代码被当作新脚本执行!
// ✅ 防御:转义危险字符
function safeJsonStringify(data) {
return JSON.stringify(data)
.replace(/</g, '\\u003c') // 转义 <
.replace(/>/g, '\\u003e') // 转义 >
.replace(/&/g, '\\u0026') // 转义 &
.replace(/'/g, '\\u0027') // 转义单引号
.replace(/"/g, '\\u0022') // 转义双引号(非 JSON 内的)
}
// 现在安全了
const safe = safeJsonStringify(userData)
// {"name":"\u003c/script\u003e\u003cscript\u003ealert(\u0022XSS\u0022)\u003c/script\u003e"}
// 浏览器不会把 \u003c/script\u003e 识别为闭合标签
陷阱三:JSON 解析 DoS
攻击者可以发送精心构造的超大或超深嵌套的 JSON,导致解析过程耗尽内存或阻塞主线程。
// ✅ 防御:在解析前做基本检查
function safeJsonParse(jsonString, options = {}) {
const { maxLength = 1024 * 1024, maxDepth = 20 } = options
// 1. 检查大小
if (jsonString.length > maxLength) {
throw new Error(`JSON 超过最大允许长度 (${maxLength} bytes)`)
}
// 2. 粗略检查嵌套深度(通过连续 { 或 [ 的数量)
let depth = 0
let maxFound = 0
for (const ch of jsonString) {
if (ch === '{' || ch === '[') maxFound = Math.max(maxFound, ++depth)
if (ch === '}' || ch === ']') depth--
}
if (maxFound > maxDepth) {
throw new Error(`JSON 嵌套深度超过限制 (${maxDepth})`)
}
// 3. 解析
return JSON.parse(jsonString)
}
// 使用
try {
const data = safeJsonParse(untrustedInput, {
maxLength: 512 * 1024, // 最大 512KB
maxDepth: 10 // 最大嵌套 10 层
})
} catch (e) {
console.error('JSON 解析被拒绝:', e.message)
}
安全检查清单
处理不受信任的 JSON 数据时,记住这个清单:1) 限制输入大小和嵌套深度;2) 过滤 __proto__、constructor、prototype 等危险属性;3) 在 SSR 中嵌入 JSON 时转义 </script>;4) 用 Object.create(null) 或 Map 代替普通对象存储不受信任数据;5) 对用户输入的 JSON 做 Schema 校验。
总结
JSON 是前端开发的基础设施,但大多数人只用了它 10% 的能力。掌握 replacer 和 reviver 可以优雅地处理序列化边界情况;理解 structuredClone 与 JSON 深拷贝的区别能帮你选对工具;JSON Schema 让运行时数据校验变得标准化;性能优化手段(Worker、流式解析、分页)让你能从容应对大数据量场景;而安全意识则是避免线上事故的最后防线。希望这些技巧能让你在日常开发中更加得心应手。如果你对开发效率工具感兴趣,推荐阅读 提升开发效率的在线工具推荐 ;如果想了解另一个常见编码格式的原理,可以看看 Base64 编码原理详解。