拒绝外部修改的对象

document 是 mongoose 的数据实例,它拥有一些比较有意思的特性

  1. 实例拥有一些方法,能被正常调用
  2. console.log 显示的是纯数据,不包含方法
  3. 实例属性可以内部增加,但不能在外部增加
  4. 实例属性在外部不可被覆写,也不能被删除
  5. 实例在执行 update 方法后,属性也能被更新

看起来,1 和 2 比较冲突,3、4 和 5 也比较冲突。所以要怎么实现呢?

1、2 实现看起来比较简单,最简单的是把方法都添加到实例创建的原型上,这样创建的 document 实例的时候会自动继承原型上的方法,而在 console.log 实例的时候不会显示继承的属性,所以显示的结果会看起来非常干净。

后面的 3、4、5 统称起来就是 实例可内部修改但不能被外部修改 ,这个实现起来就麻烦了。

创建一个不可被修改也不能被删除的对象非常地容易,将 Object.defineProperty 中的 configurablewritable 都设为 false 就可以了。但是这样创建的对象是完全不能被修改的,更新属性的话没法搞。
要支持更新属性也行啊,把 configurablewritable 都设为 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