最近在做 Node 服务端需求的时候,遇到了几次服务端报错的问题。打 log 发现均是一些 Error,但是它们都没法很好地透传给前端浏览器,出现问题只能查看服务端机器的日志,调试起来非常不方便。思考了一下,服务端的内容都是通过 JSON.stringify() 处理,然后设置
Content-type: text/json
的响应头以后再传给前端的,如果 Error 也能够被这样处理,那么调试起来就方便多了。
说到 JSON.stringify() 这个方法,相信所有玩过 JS 的同学都不会陌生。它能够方便地把一个对象转化成字符串,在不同的场景中都有着极大的用处。但是它也有一个较大的缺点,无法直接处理诸如 Error 一类的对象。
首先来看个例子:
const err = new Error('This is an error')
JSON.stringify(err)
// => "{}"
复制代码
在控制台运行上述代码后会发现,JSON.stringify() 的结果是一个字符串的
"{}"
,里面没有任何有效内容。这是否意味着 JSON.stringify() 确实无法处理 Error 呢?下面我们来看看在 MDN 里这个函数是如何定义的。
MDN 定义
首先来看看 描述
JSON.stringify()将值转换为相应的JSON格式:
列了那么多其实是为了凑字数
我们只看最后一条描述:
其他类型的对象,包括Map/Set/weakMap/weakSet,仅会序列化可枚举的属性。
“仅会序列化可枚举的属性”,是什么意思呢?众所周知,在 JS 的世界中一切皆对象,对象有着不同的属性,这些属性是否可枚举,我们用 enumerable 来定义。
对象属性的 enumerable
举个例子,我们用
obj = { a: 1, b: 2, c: 3 }
来定义一个对象,然后设置它的
c
属性为“不可枚举”,看看效果会如何:
首先看处理前的效果:
const obj = { a: 1, b: 2, c: 3 }
JSON.stringify(obj)
// => "{"a":1,"b":2,"c":3}"
复制代码
再看处理后的效果:
const obj = { a: 1, b: 2, c: 3 }
Object.defineProperty(obj, 'c', {
value: 3,
enumerable: false
JSON.stringify(obj)
// => "{"a":1,"b":2}"
复制代码
可以看到,在对
c
属性设置为不可枚举以后,JSON.stringify() 便不再对其进行序列化。
我们把问题再深入一些,有没有办法能够获取一个对象中包含不可枚举在内的所有属性呢?答案是使用
Object.getOwnPropertyNames()
方法。
依然是刚刚被改装过的
obj
对象,我们来看看它所包含的所有属性:
Object.getOwnPropertyNames(obj)
// => ["a", "b", "c"]
复制代码
不可枚举的
c
属性也被获取到了!
用同样的方法,我们来看看一个 Error 都包含哪些属性:
const err = new Error('This is an error')
Object.getOwnPropertyNames(err)
// => ["stack", "message"]
复制代码
可以看到,Error 包含了
stack
和
message
两个属性,它们均可以使用点运算符
.
从
err
实例里面拿到。
既然我们已经能够获取 Error 实例的不可枚举属性及其内容,那么距离使用 JSON.stringify() 序列化 Error 也已经不远了!
JSON.stringify() 的第二个参数
JSON.stringify() 可以接收三个参数:
JSON.stringify(value[, replacer [, space]])
value
将要序列化成 一个JSON 字符串的值。
replacer 可选
如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为null或者未提供,则对象所有的属性都会被序列化。
space 可选
指定缩进用的空白字符串,用于美化输出(pretty-print);如果参数是个数字,它代表有多少的空格;上限为10。该值若小于1,则意味着没有空格;如果该参数为字符串(字符串的前十个字母),该字符串将被作为空格;如果该参数没有提供(或者为null)将没有空格。 返回值 节 一个表示给定值的JSON字符串。
我们来看
replacer
的用法:
……如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中……
依然使用上文的
obj
为例子:
const obj = { a: 1, b: 2, c: 3 }
Object.defineProperty(obj, 'c', {
value: 3,
enumerable: false
JSON.stringify(obj, ['a', 'c'])
// => "{"a":1,"c":3}"
复制代码
可以看到,我们在
replacer
中指定了要序列化
a
和
c
属性,输出结果也是只有这两个属性的值,且不可枚举的
c
属性也被序列化了!守得云开见月明,Error 对象被序列化的方法也就出来了:
const err = new Error('This is an error')
JSON.stringify(err, Object.getOwnPropertyNames(err), 2)
// "{
// "stack": "Error: This is an error\n at <anonymous>:1:13",
// "message": "This is an error"
// }"
复制代码
后记
文章本来的标题是“你不知道的 JSON.stringify()”,但是总感觉词不达意,有标题党的嫌疑,遂改成更为实在的现标题。
对于一些常用的函数,其背后也有着许多值得探索的内容,比如这次为了让 JSON.stringify() 去序列化一个 Error,我又复习了一遍 JS 对象属性中 enumerable 的相关知识,才知道这些原本以为很底层的基础知识其实对真实业务也有着巨大的作用。夯实基础,永远都是很重要的。