标签 #Node.js

拒绝外部修改的对象

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

理解 Node.js 异步编程

事件队列

JavaScript 是单线程设计,任务的调度方式就是队列。即在运行过程中只有一个线程在运行,也就是说每次只能执行一个任务。当调用 setTimeout 这种异步函数的时候,会将控制权交给通信线程,其中的回调不会立即执行。而当调用赋值等同步语句的时候,会立即执行。JavaScript 虚拟机会轮询这个队列,执行合适的事件。

这样做有什么好处呢?

不用新开线程,减轻系统开销,再多的并发也只是加长了事件队列,也不会令执行混乱。

异步IO

一般同步执行的的 Web 程序中操作 IO 的函数总是执行时间最长的,当网络和存储技术不发生大的革新的情况下,异步IO 是一个提升响应速度的好办法。

同步IO 就是在执行 IO 操作的时候线程阻塞,不继续向下执行,而是一直等待这个 IO 操作的返回。 而当 JavaScript 虚拟机调用了异步IO函数以后,不会等待函数的返回,而是继续执行下一个事件。

那什么怎么知道异步函数执行完了呢?

IO 操作是另外的通信线程来实现的,通信线程操作结束后会将执行原始回调函数的引用加入到 JavaScript 事件队列中,JavaScript 虚拟机因为一直在轮询这个事件队列,会发现这个事件并完成后续的处理。对于 Node.js 应用而言,它是部分多线程,即非阻塞IO 为多线程,但是 IO 结果的处理还是单线程执行。

所以 Node.js 的执行过程能保证非常的迅速。

回调函数

回调函数也是一个函数,只不过它的调用方式跟普通的函数不一样,它是由事件驱动的。比如完成个某个IO 操作以后执行回调,那么这个回调这是由完成事件来触发的。

人脑最自然的思考方式是顺序执行,非常适合写同步代码。而异步理解起来会比较绕,你需要将下一个执行的事件封装成一个回调函数,然后当一个参数传递给异步函数。异步函数在完成它自己的操作以后再执行这个回调函数,而这个回调函数里面又可能有其他异步函数,里面也有回调函数,所以整个执行过程看起来会比较像一个金字塔。

比较合理的做法是将不同的回调封装成函数,在异步函数里面只做调用,这样代码的结构会清晰很多。但有时候还是会造成一些混乱,所以也有 Qasync 这些来填坑的库。

1