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。
七、关联文档
- 闭包与作用域
- 垃圾回收与内存泄漏(深拷贝与内存)
- Date 与常用工具函数