JSON.stringify
是我们在日常开发中经常会用到的一个函数。例如:用来处理 POST 请求中的 body 参数,将 object
对象存储到 localStorage
里面,甚至可以用来实现一个简单的深拷贝。
然而,就是这么一个常见的 JS 官方函数,却有着一些少有人知道的小秘密。今天,就让我们来一探究竟吧。
Secret 1:美化输出
这个可能有不少人都知道。JSON.stringify
接收一个参数来美化输出结果,让其更加可读。
默认情况下,JSON.stringify
会把序列化结果输出为一行。但当提供了第三个参数 space
的时候,JSON.stringify
会为每一个键值单起一行,并且键名key
前面附加上 space
前缀。
space
可以是数字或者字符串。
const obj = { a: 1, b: { c: 2 } }
JSON.stringify(obj)
// "{"a":1,"b":{"c":2}}"
JSON.stringify(obj, null, 2)
// "{
// "a": 1,
// "b": {
// "c": 2
// }
// }"
JSON.stringify(obj, null, '**')
// "{
// **"a": 1,
// **"b": {
// ****"c": 2
// **}
// }"
Secret 2: 神奇的 replacer
相信一定有同学很好奇上面第二个参数 null
是干嘛的。莫要着急,且听我细细讲来。
JSON.stringify
的第二个参数我们称之为 replacer
。可以是列表或者函数。
如果 replacer
是列表
当 replacer
是列表的时候,它的作用就像是白名单一样,最终结果中只会包含 key
在列表中的 key: value
。
const user = {
name: 'Hopsken',
website: 'https://hopsken.com',
password: 'SUPER_SECRET',
}
JSON.stringify(user, ['name', 'website'], 2)
// "{
// "name": "Hopsken",
// "website": "https://hopsken.com"
// }"
值得一提的是,最终结果中的键名排列会遵照列表中的先后顺序。
JSON.stringify(user, ['website', 'name'], 2)
// "{
// "website": "https://hopsken.com",
// "name": "Hopsken"
// }"
// 你甚至可以这样
const config = { ... }
JSON.stringify(config, Object.keys(config).sort(), 2)
如果 replacer
是函数
当replacer
是函数的时候,JS 会在序列化是对每个键值对调用这个函数,并且使用函数的返回值作为 key
对应的 value
。
const obj = { a: 1, b: { c: 2 } }
JSON.stringify(obj, (key, value) => {
if (typeof value === 'number') {
return value + 1;
}
return value;
}, 2)
// "{
// "a": 2,
// "b": {
// "c": 3
// }
// }"
如果返回值为 undefined
,那么最终结果中将忽略该值。(这是预期的行为,因为 JSON 标准格式中并没有 undefined
这个值)。
const user = {
name: 'Hopsken',
password: 'SUPER_SECRET',
}
JSON.stringify(user, (key, value) => {
if (key.match(/password/i)) {
return undefined;
}
return value;
}, 2)
// "{
// "name": "Hopsken"
// }"
Secret 3: 自行控制需要输出啥
JSON.stringify
的第三个秘密!当 JSON.stringify()
在尝试对一个对象进行序列化时,会先遍历这个对象,检查对象是否存在 toJSON()
这个属性。如果有的话,JSON.stringify()
将会对这个函数返回的值进行序列化,而非原来的对象。
简单来说,toJSON()
方法定义了什么值将被序列化。
通过 toJSON()
这个方法,我们可以自己去控制 JSON.stringify()
的行为。
举个例子:
const movie = {
title: '让子弹飞',
year: 2010,
stars: new Set(['周润发', '姜文', '葛优']),
toJSON() {
return {
name: `${this.title} ${this.year}`,
actors: [... this.stars]
}
}
}
JSON.stringify(movie, null, 2)
// "{
// "name": "让子弹飞 2010",
// "actors": [
// "周润发",
// "姜文",
// "葛优"
// ]
// }"
上面的例子,我们就用了 toJSON
属性来让 JSON.stringify()
来支持序列化 Set
类型数据。
值得一提的是,toJSON
中的 this
指向的是当前层级的对象,作用域也只在当前层级对象上。
const user = {
name: 'Hospken',
wechat: {
name: 'FEMinutes',
toJSON() {
return `WX: ${this.name}`;
},
}
}
JSON.stringify(user, null, 2)
// "{
// "name": "Hospken",
// "wechat": "WX: FEMinutes"
// }"
可以看到,wechat.toJSON()
只作用在了 wechat
这个属性上。
附加题: 对列表使用 JSON.stringify
?
那我们知道,JS 里面列表也是 object
。那按理说 JSON.stringify
应该也可以用在列表上。
首先,我们先来试一下默认参数下的结果。
const arr = ["apple", "orange", "banana"]
JSON.stringify(arr)
// "["apple","orange","banana"]"
一切正常。
再来试一下 space
参数:
const arr = ["apple", "orange", "banana"]
JSON.stringify(arr, null, 2)
// "[
// "apple",
// "orange",
// "banana"
// ]"
Perfect!
那 replacer
呢?
const arr = ["apple", "orange", "banana"]
JSON.stringify(arr, (key, value) => {
return `one ${value}`
}, 2)
// ""one apple,orange,banana""
凉凉,似乎不行啊。。。看上去 replacer
函数只执行了一次,而不是预期中的三次。但是,本来 JSON.stringify()
也不是这么用的,出现预期以外的结果也是正常的。
但是没关系,咱们还有 toJSON()
啊!让我们试一下:
const arr = ["apple", "orange", "banana"]
arr.toJSON = function() {
return this.slice().map(val => `one ${val}`)
}
JSON.stringify(arr, null, 2)
// "[
// "apple",
// "orange",
// "banana"
// ]"
这样咱们就通过 toJSON()
实现了 replacer
的作用。岂不美哉。
尾声
那么今天的探秘就到这里啦。篇幅限制,关于 JSON.stringify()
对边界情况的处理(如循环引用)咱们就不详细展开讲啦。
感谢各位的观看~咱们下期再见~
参考资料
ECMAScript 2015 (6th Edition, ECMA-262)