JSON.stringify 的那些小秘密

2,261 阅读3分钟

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() 对边界情况的处理(如循环引用)咱们就不详细展开讲啦。

感谢各位的观看~咱们下期再见~

参考资料

JSON.stringify() - MDN

ECMAScript 2015 (6th Edition, ECMA-262)

The 80/20 Guide to JSON.stringify in JavaScript

The secret power of JSON stringify