在我刚开始学习web开发时,JSON是看起来很简单的一个东西。因为JSON字符串看起来就像一个文本,JavaScript对象的的最小子集。在我职业生涯的早期,我从来没有花时间去好好研究这种数据格式。我仅仅只是使用
JSON.stringify
和
JSON.parse
,直到出现意外的错误。
在这篇文章中,我想:
总结一下我在JavaScript中使用JSON(更确切的说是
JSON.stringify
API)时遇到的怪事
通过从头开始实现JSON.stringify的简化版本,来加深我对JSON的理解
什么是JSON
JSON是
Douglas Crockford
发明的一种数据结构。你可能已经知道了这些。但是有意思的是,正如Crockford在他的书《JavaScript悟道》中写的那样,他承认:“关于JSON的最糟糕的事情就是名字。”
JSON表示JavaScript对象表示法(JavaScript Object Notation)。问题在于,这个名字误导人们认为它只适用于JavaScript。然而事实上,它的目的是允许不同语言编写的程序有效地沟通。
在类似的问题上,Crockford也坦言,JavaScript提供的两个内置API可以与JSON一起工作。它们是
JSON.parse
和
JSON.stringify
,同样的,命名也很糟糕。它们应该分别被称为
JSON.decode
和
JSON.encode
,因为
JSON.parse
需要一个JSON文本并将其
解码
为JavaScript值,而
JSON.stringify
需要一个JavaScript值并将其
编码
为JSON文本/字符串。
说完了命名,让我们看看JSON支持哪些数据类型,以及当一个不兼容的JSON值被
JSON.stringify
字符串化时会发生什么。
JSON支持哪些数据格式
JSON有一个
官方网站
,你可以在上面查看所有支持的数据类型,但是说实话,对于我来说,页面上的图有点难以理解。所以我更喜欢下面的类型注释:
type Json =
| null
| boolean
| number
| string
| Json[]
| {[key: string]: Json}
对于任何不属于上述Json
联合类型的数据类型,比如说undefined
, Symbol
, BigInt
,以及其他内置对象,比如说Function
, Map
, Set
, Regex
,它们不被JSON支持,注释也一样不被支持。
下一个合乎逻辑的问题是,在JavaScript的上下文中,当我们说一个数据类型不被JSON支持时,到底是什么意思?
JSON.stringify的怪异行为
在JavaScript中,通过JSON.stringify
将值转换为JSON字符串。
对于JSON支持的类型的值,它们会被转换为预期的字符串:
JSON.stringify(1)
JSON.stringify(null)
JSON.stringify('foo')
JSON.stringify({foo: 'bar'})
JSON.stringify(['foo', 'bar'])
但在字符串化/编码过程中,如果涉及到不支持的类型,事情会变得棘手起来。
当直接传递不支持的类型undefined
, Symbol
, 和 Function
时,JSON.stringify
会输出undefined
(不是'undefined'
字符串):
JSON.stringify(undefined)
JSON.stringify(Symbol('foo'))
JSON.stringify(() => {})
对于其他内置对象类型(Function
和 Date
除外),比如说Map
, Set
, WeakMap
, WeakSet
, Regex
等等,JSON.stringify
会返回一个空对象字面量的字符串,也就是'{}'
:
JSON.stringify(/foo/)
JSON.stringify(new Map())
JSON.stringify(new Set())
当被序列化的值位于数组或对象中时,会发生更多不一致的行为。
对于不支持的导致undefined
的类型,也就是undefined
, Symbol
, Function
,当它们在数组中被发现时,会被转换为字符串'null'
;当它们在对象中被发现时,整个属性会从输出中省略:
JSON.stringify([undefined])
JSON.stringify({foo: undefined})
JSON.stringify([Symbol()])
JSON.stringify({foo: Symbol()})
JSON.stringify([() => {}])
JSON.stringify({foo: () => {}})
另一方面,对于其他内置对象类型,诸如Map
, Set
, Regex
等,存在于数组或对象中时,被JSON.stringify
转换完毕后,都会变为空对象字面量的字符串,也就是'{}'
:
JSON.stringify([/foo/])
JSON.stringify({foo: /foo/})
JSON.stringify([new Set()])
JSON.stringify({foo: new Set()})
JSON.stringify([new Map()])
JSON.stringify({foo: new Map()})
对于最近添加的新类型BigInt
,JSON.stringify
会抛出一个TypeError
错误 。另一种情况时,当传递循环对象时,JSON.stringify
会抛出错误。大多数情况下,JSON.stringify
是相当宽容的。它不会因为你违反了JSON的规则而使你的程序崩溃(除非是BigInt或循环对象)。
const foo = {}
foo.a = foo
JSON.stringify(foo)
JSON.stringify(BigInt(1234567890))
尽管是数字类型,NaN
和Infinity
依然会被JSON.stringify
转换为null
。这个设计决定背后的原因是,正如Crockford在他的书《JavaScript悟道》中写到的,NaN
和Infinity
的存在表明了一个错误。他通过使它们变成null
来排除它们。
JSON.stringify(NaN)
JSON.stringify(Infinity)
通过JSON.stringify
,Date
对象会被编码为ISO字符串,因为具有Date.prototype.toJSON
。
JSON.stringify(new Date())
JSON.stringify
只处理可枚举的、非符号键的对象属性。符号键、非枚举属性会被忽略:
const foo = {}
foo[Symbol('p1')] = 'bar'
Object.defineProperty(foo, 'p2', {value: 'baz', enumerable: false})
JSON.stringify(foo)
顺便说一下,希望你能明白为什么使用JSON.parse
和JSON.stringify
来深克隆一个对象大多是一个坏主意。
我知道要记住的东西很多,所以我整理了一份小抄,供你参考。
自定义编码
目前为止,我们所讨论的是,JavaScript如何通过JSON.stringify
将值编码为JSON字符串的默认行为,有两种方式可以自行控制转换规则:
添加一个toJSON
方法,到你传递给JSON.stringify
的对象上。这也是为什么Date
对象传递给JSON.stringify
不会导致一个空对象字面量。因为Date
对象会从它的原型上继承toJSON
方法。
const foo = {
toJSON: () => 'bar',
JSON.stringify(foo)
JSON.stringify
接收一个称为replacer
的可选参数,它可以是一个函数或一个数组,来改变字符串化过程的默认行为。
简化版JSON.stringify
下面是简化版JSON.stringify
的实现。为了简洁起见,这里省略了可选参数replacer
和 space
。
const isCyclic = (input) => {
let seen = new Set()
const dfs = (obj) => {
if (typeof obj !== 'object' || obj === null) return false
seen.add(obj)
return Object.entries(obj).some(([key, value]) => {
const result = seen.has(value) ? true : isCyclic(value)
seen.delete(value)
return result
return dfs(input)
function jsonStringify(data) {
if (isCyclic(data))
throw new TypeError('Converting circular structure to JSON')
if (typeof data === 'bigint')
throw new TypeError('Do not know how to serialize a BigInt')
if (data === null) {
return 'null'
const type = typeof data
if (type !== 'object') {
let result = data
if (Number.isNaN(data) || data === Infinity) {
result = 'null'
} else if (
type === 'function' ||
type === 'undefined' ||
type === 'symbol'
return undefined
} else if (type === 'string') {
result = '"' + data + '"'
return String(result)
if (type === 'object') {
if (typeof data.toJSON === 'function') {
return jsonStringify(data.toJSON())
if (data instanceof Array) {
let result = []
data.forEach((item, index) => {
typeof item === 'undefined' ||
typeof item === 'function' ||
typeof item === 'symbol'
result[index] = 'null'
} else {
result[index] = jsonStringify(item)
result = '[' + result + ']'
return result.replace(/'/g, '"')
} else {
let result = []
Object.keys(data).forEach((item) => {
if (typeof item !== 'symbol') {
data[item] !== undefined &&
typeof data[item] !== 'function' &&
typeof data[item] !== 'symbol'
result.push('"' + item + '"' + ':' + jsonStringify(data[item]))
return ('{' + result + '}').replace(/'/g, '"')