29

image

最近在做 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格式:

  • 转换值如果有toJSON()方法,该方法定义什么值将被序列化。
  • 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
  • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
  • undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined被单独转换时,会返回undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined).
  • 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
  • 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
  • Date日期调用了toJSON()将其转换为了string字符串(同Date.toISOString()),因此会被当做字符串处理。
  • NaN和Infinity格式的数值及null都会被当做null。
  • 其他类型的对象,包括Map/Set/weakMap/weakSet,仅会序列化可枚举的属性。

列了那么多其实是为了凑字数我们只看最后一条描述:

其他类型的对象,包括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 包含了 stackmessage 两个属性,它们均可以使用点运算符 .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 中指定了要序列化 ac 属性,输出结果也是只有这两个属性的值,且不可枚举的 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 的相关知识,才知道这些原本以为很底层的基础知识其实对真实业务也有着巨大的作用。夯实基础,永远都是很重要的。


jrainlau
12.9k 声望11.7k 粉丝

Hiphop dancer,