背景:云函数开发中,日志的打印直接通过
console.log
,这些日志会被自动采集。而打印对象前,先使用JSON.stringify(obj)
将对象转换为相应的字符串。问题由此产生。
先看下面的代码
const err = new Error('I am error');
const errStr = JSON.stringify(err);
console.log(errStr);
// {}
这里并没有如我们预期的一样输出含有错误信息的字符串,而是打印了空对象对应的花括号。
MDN上关于JSON.stringify的序列化规则如下:
转换值如果有 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,仅会序列化可枚举的属性。
从最后一条规则可以看出,JSON.stringify
只会对可枚举
的属性进行序列化。
回到最初的问题,对Error的实例进行json序列化,为何会输出代表空对象的花括号呢?
写一个简单的测试输出
const err = new Error('demo')
console.log(err.prototype.propertyIsEnumerable('name'))
console.log(err.prototype.propertyIsEnumerable('message'))
console.log(err.prototype.propertyIsEnumerable('stack'))
// false
// false
// false
Error
的实例中,用于log的几个属性都是不可枚举的。因此输出花括号也就在情理之中。但是如何把Error
中的信息序列化成字符串呢?
方案也可以从上面序列化的原则中推导出来
- 让属性变成可枚举
- 实现
toJSON
方法
- 实现
修改属性的可枚举特性
const err = new Error('demo');
Object.defineProperty(err, 'message', {
enumerable: true,
});
Object.defineProperty(err, 'stack', {
enumerable: true,
});
很容易联想到使用Object.defineProperty修改属性的相关特性,通过上面的修改后就可以序列化指定的属性及相应的值。
可是Error
在不同的运行环境(node/浏览器 .etc
)中,其属性值列表的支持性有些许差别,如何能在不需要知道有哪些属性需要枚举的前提下,实现序列化呢?可以使用toJSON
结合getOwnPropertyNames
,构造序列化的字符串数据。
实现toJSON方法
先看看Object.getOwnPropertyName在MDN上的解释
Object.getOwnPropertyNames() returns an array whose elements are strings corresponding to the enumerable and non-enumerable properties found directly in a given object obj
这个方法会返回给定对象的字符串类型
属性(包含可枚举和不可枚举)列表,而在ES6+,数值类型
的属性也会返回。基于此,在toJSON
方法中将其返回为一个属性均为可枚举的对象就可以了。
Error.prototype.toJSON = function() {
const keys = Object.getOwnPropertyNames(this);
const obj = Object.create(null);
keys.forEach((k) => {
obj[k] = err[k];
});
return obj;
};
const err = new Error('demo')
console.log(JSON.stringify(err))
// error stack message
不仅Error
如此,对于其他存在不可枚举属性的对象道理也是相通,属性是否可枚举不仅影响到对象的序列化,对于其他一些运算(如解构运算
)同样会有影响,在日常开发中需要小心谨慎,对API的特性了解得更为清晰才能少踩坑。