很久之前就对 Mongoose 的 toObject 和 toJSON 有什么区别很感兴趣,今天发现了个 github1s 可以快捷地对 Github 源码进行浏览,就顺便来探寻一下这个问题。
我们来看看两者的实现:
Document.prototype.toJSON = function(options) {
return this.$toObject(options, true);
};
Document.prototype.toObject = function(options) {
return this.$toObject(options);
};
可以看到,两者的内部实现就差了一个 boolean。
在 JSDoc 里面,Mongoose 是这么描述 $toObject 的:
不操纵选项的
toObject()和toJSON()的内部帮助程序
简单看一下这个函数,发现我们刚才看到的 boolean 实际上是 $toObject 的第二个参数 json,为真时代表是 toJSON,默认则是 toObject。
Document.prototype.$toObject = function(options, json) {...}
继续追踪下去,可以发现 json 首先是用来判断 options 存储的位置:
const path = json ? 'toJSON' : 'toObject';
const baseOptions = get(this, 'constructor.base.options.' + path, {});
const schemaOptions = get(this, 'schema.options', {});
// merge base default options with Schema's set default options if available.
// `clone` is necessary here because `utils.options` directly modifies the second input.
defaultOptions = utils.options(defaultOptions, clone(baseOptions));
defaultOptions = utils.options(defaultOptions, clone(schemaOptions[path] || {}));
...
接着是用来传给 clone 函数来对 this._doc 进行克隆的:
const cloneOptions = Object.assign(utils.clone(options), {
_isNested: true,
json: json,
minimize: _minimize
});
...
let ret = clone(this._doc, cloneOptions) || {};
...
return ret;
这个就是关键函数了,那这个 clone 是哪来的呢?
我们可以看到,是引用自 utils.clone:
const utils = require('./utils');
const clone = utils.clone;
而 utils.js 又引用自
const clone = require('./helpers/clone');
七拐八拐,我们终于在 lib/helpers/clone.js 看到了这个函数的真容。
function clone(obj, options, isArrayChild) {}
这个函数的逻辑是这样的:
obj == null就返回;obj是数组就让cloneArray返回;obj是MongooseObject就进行深层判断,有json参数且有toJSON方法就调用,反之返回toObject;obj有constructor就进行分类判断,分别调用cloneObject、new Date()和cloneRegExp,都不符合就继续;obj是ObjectId实例就返回new ObjectId(obj.id);obj是Decimal128就进行处理并返回;obj有symbols.schemaTypeSymbol就调用obj的clone方法返回;- 设置有
bson且obj有toBSON就直接返回obj; obj有valueOf就调用并返回;- 最终一个都没命中,就按
object的来返回cloneObject(obj, options, isArrayChild)。
可以看到,这个函数其实就是对值不断遍历根据设置取值的过程,里面关于 json 参数的只有寥寥可数的几行:
if (isMongooseObject(obj)) {
//...
if (options && options.json && typeof obj.toJSON === 'function') {
return obj.toJSON(options);
}
return obj.toObject(options);
}
也就是说,只有在 isMongooseObject 的情况下,才会调用 toJSON,那么这个函数又是什么呢,我们来看看:
/*!
* Returns if `v` is a mongoose object that has a `toObject()` method we can use.
*
* This is for compatibility with libs like Date.js which do foolish things to Natives.
*
* @param {any} v
* @api private
*/
module.exports = function(v) {
if (v == null) {
return false;
}
return v.$__ != null || // Document
v.isMongooseArray || // Array or Document Array
v.isMongooseBuffer || // Buffer
v.$isMongooseMap; // Map
}
里面写到,这个函数可以判断这个对象是不是 Mongoose object(Document,Array or Document Array,Buffer,Map),有没有 toObject() 可以用。
其中,Document 的 toJSON 和 toObject() 我们已经看过了,最终就是返回 clone 过的 this._doc。至于其他的,我看了一下,MongooseArray 和 MongooseBuffer 都只有对 toObject 的特殊定义,只有 Map 有差异。
对 Map 来说, toJSON 和 toObject() 的差异就在于, toJSON 会默认应用 flattenMaps 设置。flattenMaps 会把 Map 转换成一个普通对象,就像这样:
>> mapProp.toJSON()
{ a: 1, b: 2 }
>> mapProp.toObject()
Map(2) { size: 2, a => 1, b => 2 }
由于 Mongoose 的 Map 不是真 Map,只支持字符串做 Key,所以序列化起来还是很容易的。
众所周知,JSON.stringify 在对对象序列化的时候,会调用对象的 toJSON 方法,所以说,Mongoose 之所以设置这个只有一些不一样的 toJSON,主要还是让开发者可以控制 JSON.stringify 时对象的行为。
假设你想直接重写 Schema 的 toJSON 的方法,可以这么做:
userSchema.methods.toJSON = function () {
const user = this.toObject();
delete user.password;
return user;
};
或者你想要从 toJSON 的结果上修改:
const { Document } = require('mongoose');
userSchema.methods.toJSON = function () {
const user = Document.prototype.toJSON.call(this);
delete user.password;
return user;
};
这样例如 Express 等框架在对你的对象进行 JSON.stringify 的时候,就可以给你一个满意的结果了!