document
是 mongoose 的数据实例,它拥有一些比较有意思的特性
- 实例拥有一些方法,能被正常调用
-
console.log
显示的是纯数据,不包含方法 - 实例属性可以内部增加,但不能在外部增加
- 实例属性在外部不可被覆写,也不能被删除
- 实例在执行
update
方法后,属性也能被更新
看起来,1 和 2 比较冲突,3、4 和 5 也比较冲突。所以要怎么实现呢?
1、2 实现看起来比较简单,最简单的是把方法都添加到实例创建的原型上,这样创建的 document
实例的时候会自动继承原型上的方法,而在 console.log
实例的时候不会显示继承的属性,所以显示的结果会看起来非常干净。
后面的 3、4、5 统称起来就是 实例可内部修改但不能被外部修改 ,这个实现起来就麻烦了。
创建一个 不可被修改也不能被删除 的对象非常地容易,将 Object.defineProperty 中的 configurable
和 writable
都设为 false
就可以了。但是这样创建的对象是完全不能被修改的,更新属性的话没法搞。
要支持更新属性也行啊,把 configurable
和 writable
都设为 true
... orz,这样就能外部修改了。
还有个办法就是用 getter
,听起来不错,但是实际使用的时候发现 configurable
不能设为 false
,不然的话不能更新 getter
里面的数据,设为 true
以后数据虽然不能被更新但又能被删除。
而且,设置了 getter
以后,用 console.log
打印实例的时候属性会显示成 [Getter]
,而不是实际值,这样跟 2 又冲突了。
还有个办法 Object.seal 可以密封对象。 被密封后的对象不能添加新的属性,不能删除已有属性,以及不能修改已有属性的可枚举性、可配置性、可写性,但可以修改已有属性的值的对象 。orz ... 这里的修改并不能限制外部的。
似乎可以综合一下, Object.seal
允许修改, getter
可以限制修改来源。
而实际上 getter
的数据更新可以指定到一个内部变量,这样只用更改这个变量就可以更改这个属性了,而不用去直接改 getter
,类似于做了一个映射。
class Model constructor: (record) -> _.extend @, record: record _.keys(record).map (key) => Object.defineProperty @, key, enumerable: true configurable: false get: -> @record[key] update: (record) -> _.extend @, record: record
这样处理的话已经成功了一部分了,再还要不允许往实例上添加属性,在构造函数里面加上 Object.seal @
就好了。
后面的问题处理完了,但是前面的 1、2 又冲突了。添加的 record
属性会在 console.log
的时候显示出来,而添加的 getter
也会显示成了 [Getter]
。
去研究了下 mongoose 的实现,发现它在 document
的原型里面添加了一个 inspect 方法。
我和逸川一起追溯了一下 console.log 的在 node.js 中的实现,整个的流程就是
-
console.log
调用了util.format
- 然后进而调用了
util.inspect
-
util.inspect
又调用了formatValue
- 然后是
formatValue
调用了传入console.log
的参数的inspect
方法
所以,console.log 的传入参数的 inspect 方法会直接影响 console.log 显示的内容。
class Display constructor: -> inspect: -> return 'hello' console.log new Display() # 会显示 hello
那我们再给我们的 Model 加上 inspect
方法,里面返回 record
的数据即可。
这样我们就实现了一个 可内部修改但不能被外部修改 的对象了。
完整的示例参见 cado 。