开发工具2026-03-18 9 分钟

JSON 不只是格式化:前端开发者必知的数据处理技巧

别只会 JSON.parseJSON.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 方案甚至更通用。但如果数据包含 DateMapSet、循环引用等,优先使用 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__constructorprototype 等危险属性;3) 在 SSR 中嵌入 JSON 时转义 </script>;4) 用 Object.create(null)Map 代替普通对象存储不受信任数据;5) 对用户输入的 JSON 做 Schema 校验。

总结

JSON 是前端开发的基础设施,但大多数人只用了它 10% 的能力。掌握 replacerreviver 可以优雅地处理序列化边界情况;理解 structuredClone 与 JSON 深拷贝的区别能帮你选对工具;JSON Schema 让运行时数据校验变得标准化;性能优化手段(Worker、流式解析、分页)让你能从容应对大数据量场景;而安全意识则是避免线上事故的最后防线。希望这些技巧能让你在日常开发中更加得心应手。如果你对开发效率工具感兴趣,推荐阅读 提升开发效率的在线工具推荐 ;如果想了解另一个常见编码格式的原理,可以看看 Base64 编码原理详解