Mongoose 的 toObject 和 toJSON 有什么区别?

1,632 阅读1分钟

很久之前就对 Mongoose 的 toObjecttoJSON 有什么区别很感兴趣,今天发现了个 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) {}

这个函数的逻辑是这样的:

  1. obj == null 就返回;
  2. obj 是数组就让 cloneArray 返回;
  3. objMongooseObject 就进行深层判断,有 json 参数且有 toJSON 方法就调用,反之返回 toObject
  4. objconstructor 就进行分类判断,分别调用 cloneObjectnew Date()cloneRegExp,都不符合就继续;
  5. objObjectId 实例就返回 new ObjectId(obj.id)
  6. objDecimal128 就进行处理并返回;
  7. objsymbols.schemaTypeSymbol 就调用 objclone 方法返回;
  8. 设置有 bsonobjtoBSON 就直接返回 obj
  9. objvalueOf 就调用并返回;
  10. 最终一个都没命中,就按 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(DocumentArray or Document ArrayBufferMap),有没有 toObject() 可以用。

其中,DocumenttoJSONtoObject() 我们已经看过了,最终就是返回 clone 过的 this._doc。至于其他的,我看了一下,MongooseArrayMongooseBuffer 都只有对 toObject 的特殊定义,只有 Map 有差异。

Map 来说, toJSONtoObject() 的差异就在于, 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 时对象的行为。

假设你想直接重写 SchematoJSON 的方法,可以这么做:

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