很久之前就对 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
的时候,就可以给你一个满意的结果了!