Skip to content

JSON 与 URL 编码规范

作者:青见春山
发表于:2026-07-29
字数统计:6500 字
预计阅读22分钟

涵盖 JSON.stringify 的九大转换规则与七宗罪、encodeURI vs encodeURIComponent vs escape 的差异、深拷贝陷阱。

一、JSON.stringify 九大转换规则

规则一:基本类型直接转换

JavaScript
JSON.stringify(undefined)         // undefined(注意:不是字符串"undefined")
JSON.stringify(null)              // 'null'
JSON.stringify(NaN)               // 'null'
JSON.stringify(Infinity)          // 'null'
JSON.stringify(-Infinity)         // 'null'
JSON.stringify(123)               // '123'
JSON.stringify('hello')           // '"hello"'
JSON.stringify(true)              // 'true'
JSON.stringify(false)             // 'false'

规则二:非数组对象的属性顺序

JavaScript
const obj = { b: 1, a: 2, c: 3 }
JSON.stringify(obj)  // '{"b":1,"a":2,"c":3}'
// 说明:非数组对象的属性顺序是"插入顺序",整数属性会被排到前面并按数值顺序排序

const obj2 = { 3: 'three', 1: 'one', 2: 'two' }
JSON.stringify(obj2)  // '{"1":"one","2":"two","3":"three"}'
// 整数属性(看起来像数组下标的)会按数值升序排在最前

规则三:数组序列化

JavaScript
JSON.stringify([1, 2, 3])                // '[1,2,3]'
JSON.stringify([undefined, null, NaN])    // '[null,null,null]'
// 数组中的 undefined / 函数会被转为 null

JSON.stringify([
  { name: '张三', age: undefined },
  { name: '李四', age: 20 }
])
// '[{"name":"张三":{},"name":"李四","age":20}]'
// 对象的 undefined 会被忽略

规则四:toJSON 方法优先级最高

JavaScript
const obj = {
  name: '张三',
  toJSON() {
    return 'custom'
  }
}
JSON.stringify(obj)  // '"custom"',不走默认序列化

// Date 类型就是用 toJSON 实现的
JSON.stringify(new Date())  // '"2024-01-15T..."',输出 ISO 字符串

// 自定义 toJSON 常用于隐藏敏感字段
const user = {
  name: 'king',
  password: '123456',
  toJSON() {
    const { password, ...rest } = this
    return rest
  }
}
JSON.stringify(user)  // '{"name":"king"}'

规则五:第二个参数(replacer)

JavaScript
// 1. 数组形式:作为白名单(只保留指定字段)
const data = { name: '张三', age: 18, password: '123' }
JSON.stringify(data, ['name', 'age'])
// '{"name":"张三","age":18}'

// 2. 函数形式:自定义转换逻辑
JSON.stringify(data, (key, value) => {
  if (key === 'password') return undefined  // undefined 会被忽略
  if (typeof value === 'number') return value * 2
  return value
})
// '{"name":"张三","age":36}'

// 关键点:replacer 会被先调用在最外层,key 是空字符串 ""
JSON.stringify({ a: 1 }, (k, v) => {
  console.log(k, v)
  return v
})
// "" {a:1}
// "a" 1

规则六:第三个参数(space)格式化输出

JavaScript
const obj = { name: '张三', age: 18 }

// 数字:缩进空格数(最多 10)
JSON.stringify(obj, null, 2)
// '{\n  "name": "张三",\n  "age": 18\n}'

// 字符串:直接作为缩进字符
JSON.stringify(obj, null, '  ')
// '{\n  "name": "张三",\n  "age": 18\n}'

规则七:特殊值(symbol / function / undefined)

JavaScript
JSON.stringify(Symbol('foo'))           // undefined
JSON.stringify(function fn() {})         // undefined
JSON.stringify({ a: Symbol(), b: function() {} })
// '{}'
// 对象中的 Symbol / 函数会被忽略

JSON.stringify([Symbol(), function() {}])
// '[null,null]'
// 数组中的 Symbol / 函数被转为 null

规则八:循环引用会抛错

JavaScript
const obj = { name: '张三' }
obj.self = obj  // 循环引用
JSON.stringify(obj)
// TypeError: Converting circular structure to JSON

// 解决方案:用 replacer + WeakSet 记录已访问对象
function safeStringify(obj) {
  const seen = new WeakSet()
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]'
      seen.add(value)
    }
    return value
  })
}

规则九:Date / RegExp / Error 特殊处理

JavaScript
JSON.stringify(new Date('2024-01-15'))  // '"2024-01-15T00:00:00.000Z"'
JSON.stringify(/foo/)                    // '{}'(正则被序列化为空对象!)
JSON.stringify(new Error('boom'))        // '{}'(Error 也是空对象)

// 自定义 toJSON 来处理正则
RegExp.prototype.toJSON = RegExp.prototype.toString
JSON.stringify(/foo/g)  // '"/foo/g"'

二、JSON.stringify 七宗罪

1. 序列化 undefined

JavaScript
JSON.stringify(undefined)        // undefined
JSON.stringify({ a: undefined })  // '{}'
JSON.stringify([undefined])       // '[null]'

2. 序列化 NaN / Infinity

JavaScript
JSON.stringify(NaN)        // 'null'
JSON.stringify(Infinity)   // 'null'

3. 序列化函数

JavaScript
JSON.stringify(function() {})  // undefined
JSON.stringify({ fn: () => 1 })  // '{}'

4. 序列化 Symbol

JavaScript
JSON.stringify(Symbol())         // undefined
JSON.stringify({ s: Symbol() })  // '{}'

5. 默认只序列化自身可枚举属性

JavaScript
const obj = Object.create({ inherited: 'yes' }, {
  own: { value: 'me', enumerable: true }
})
JSON.stringify(obj)  // '{"own":"me"}'

6. toJSON 自定义转换陷阱

JavaScript
const obj = {
  date: new Date('2024-01-15'),
  toJSON() {
    return { custom: true }
  }
}
JSON.stringify(obj)  // '{"custom":true}',连 date 都没了

7. 性能问题:循环引用

JavaScript
const a = {}
const b = { a }
a.b = b
JSON.stringify(a)  // 抛错

三、JSON.parse 反序列化

基本用法

JavaScript
JSON.parse('{"name":"张三","age":18}')
// { name: '张三', age: 18 }

// 第二个参数是 reviver,类似 map
JSON.parse('{"name":"张三","age":18}', (key, value) => {
  if (key === 'age') return value + 1
  return value
})
// { name: '张三', age: 19 }

安全问题:JSON.parse 不会执行代码

JavaScript
// ❌ 永远不要用 eval / new Function 解析 JSON
eval('{"name":"张三"}')              // SyntaxError
new Function('return {"name":"张三"}')()  // SyntaxError

// ✅ 永远用 JSON.parse
JSON.parse('{"name":"张三"}')         // 合法

// JSON.parse 的原型污染风险(默认安全,但代码层面要注意)
const malicious = '{"__proto__":{"polluted":true}}'
JSON.parse(malicious)
// Object.prototype 不会受影响,但要注意递归合并的场景

处理日期

JavaScript
// 通常需要约定格式或自定义 reviver
const json = '{"date":"2024-01-15T00:00:00.000Z"}'
const data = JSON.parse(json, (key, value) => {
  if (key === 'date') return new Date(value)
  return value
})
data.date instanceof Date  // true

四、URL 编码三大函数

1. escape(已废弃)

JavaScript
escape('hello world')        // 'hello%20world'
escape('中文')                // '%u4E2D%u6587'
// 特点:对所有非 ASCII 字符处理不一致,**已废弃**(只在一些老旧 API 中使用)

2. encodeURI

JavaScript
encodeURI('http://example.com/path?name=张三&age=18')
// 'http://example.com/path?name=%E5%BC%A0%E4%B8%89&age=18'

// 特点:编码整个 URL,但**保留**以下字符:
// A-Z a-z 0-9 ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) #

encodeURI(';,/?:@&=+$#')  // ';,/?:@&=+$#'(这些都没被编码)
encodeURI('中文')          // '%E4%B8%AD%E6%96%87'

3. encodeURIComponent

JavaScript
encodeURIComponent('name=张三&age=18')
// 'name%3D%E5%BC%A0%E4%B8%89%26age%3D18'

// 特点:编码 URL 组件,**比 encodeURI 更彻底**,会把以下字符也编码:
// 任何 ; , / ? : @ & = + $ # 这些保留字符

4. 三者对比

JavaScript
const str = 'http://example.com/path?name=张三&age=18'

escape(str)                  // 'http%3A//example.com/path%3Fname%3D%u5F20%u4E09%26age%3D18'
encodeURI(str)               // 'http://example.com/path?name=%E5%BC%A0%E4%B8%89&age=18'
encodeURIComponent(str)      // 'http%3A%2F%2Fexample.com%2Fpath%3Fname%3D%E5%BC%A0%E4%B8%89%26age%3D18'

// 解码
decodeURI('http://example.com/path?name=%E5%BC%A0%E4%B8%89')
// 'http://example.com/path?name=张三'

decodeURIComponent('name%3D%E5%BC%A0%E4%B8%89')
// 'name=张三'

5. 实际使用场景

JavaScript
// 场景1:编码查询参数
const params = { name: '张三&李四', age: 18 }

// 错误的做法
window.location.href = `/search?name=${params.name}&age=${params.age}`
// 实际请求:/search?name=张三&李四&age=18 → 参数被截断!

// 正确的做法
const search = `name=${encodeURIComponent(params.name)}&age=${params.age}`
window.location.href = `/search?${search}`

// 场景2:使用 URLSearchParams(推荐)
const sp = new URLSearchParams()
sp.set('name', '张三&李四')
sp.set('age', '18')
sp.toString()  // 'name=%E5%BC%A0%E4%B8%89%26%E6%9D%8E%E5%9B%9B&age=18'

// 场景3:完整 URL 用 encodeURI
const url = 'http://example.com/path?q=中文'
encodeURI(url)  // 'http://example.com/path?q=%E4%B8%AD%E6%96%87'
// 单个参数值用 encodeURIComponent

五、JSON 与深拷贝

JavaScript
// JSON.parse(JSON.stringify(obj)) 是经典的"深拷贝"方法
// 但它有"七宗罪"中的所有局限 + 性能问题

const obj = {
  date: new Date(),
  fn: function() {},
  und: undefined,
  nan: NaN,
  inf: Infinity
}

const copy = JSON.parse(JSON.stringify(obj))
// {
//   date: '2024-01-15T...',  // Date 变字符串
//   nan: null,                // NaN 变 null
//   inf: null                 // Infinity 变 null
//   // fn、und 直接丢失
// }

真正可用的深拷贝

JavaScript
// 1. structuredClone(推荐,浏览器和 Node 17+ 都支持)
const copy1 = structuredClone(obj)
// ✅ Date 转 Date
// ✅ Map、Set、RegExp、ArrayBuffer 都支持
// ❌ 函数、Symbol、原型链仍会丢失

// 2. 手写递归(适合需要处理特殊场景)
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj
  if (obj instanceof Date) return new Date(obj)
  if (obj instanceof RegExp) return new RegExp(obj)
  if (hash.has(obj)) return hash.get(obj)

  const cloneObj = Array.isArray(obj) ? [] : {}
  hash.set(obj, cloneObj)

  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      cloneObj[key] = deepClone(obj[key], hash)
    }
  }
  return cloneObj
}

六、面试高频问答

Q1: JSON.stringify 的 toJSON 方法有什么用?

toJSON 是对象的一个方法,如果存在,JSON.stringify 会优先调用它来决定如何序列化这个对象。常用于:

  • 隐藏敏感字段(密码、token)
  • 自定义 Date、RegExp 等内置对象的序列化结果
  • 减少冗余数据

Q2: 为什么 JSON.stringify 不支持循环引用?

:JSON 是文本格式,本身不持有引用关系,循环引用会导致无限递归。JSON.stringify 检测到循环引用会直接抛 TypeError: Converting circular structure to JSON。解决方案是用 replacer 配合 WeakSet 记录已访问的对象,遇到重复时返回占位符(如 '[Circular]')。

Q3: encodeURI 和 encodeURIComponent 怎么选?

  • encodeURI:用于整个 URL,会保留 URL 结构字符(/ ? : @ & = + $ # 等)
  • encodeURIComponent:用于URL 中的单个参数值(如 query string 的 value),会把所有保留字符都编码

简单记忆:encodeURI 保留 URL 结构,encodeURIComponent 啥都编码。

Q4: JSON.parse 为什么是安全的?

:JSON 是纯数据格式,JSON.parse 不会执行任何代码,它只识别有效的 JSON 语法。这与 eval 不同,eval 会执行任何 JavaScript 表达式(包括函数调用、变量声明等),存在安全风险。所以永远不要用 eval 解析 JSON

七、关联文档