前端要给力之:原子,与原子联结的友类、友函数

JavaScript中的原子(Atom)是QoBean中提出的一个重要概念,借鉴自erlang,但具有与后者不同的含义。在QoBean里,Meta(元)与Atom(原子)是一对概念,前者表明执行系统中的最小单位,后者表明数据系统中的最小单位。QoBean约定这两个东西为一切元编程的初始,即最小化的执行系统与数据系统模型。

有什么意义呢?没什么意义。这只具备理论上的完整性。为了描述这种完整性,QoBean写了两个相当无厘头的函数:

// Atom system  
//  -  atom object for data  
function Atom(atom) {  
  return atom || {};  
}  
// Meta system  
//  -  meta functional for code  
function Meta(func, baseMeta) {  
  func.meta = baseMeta || arguments.callee;  
  return func;  
}  
// meta is meta for self.  
//   Meta = Meta(Meta);  
Meta(Meta);  

好了。接下来的一切故事,从Atom开始,至于Meta(),我们今后再讲。

一、原子

Atom()函数只有一行代码,即:

return atom || {}

atom是传入的参数。如果有该参数,则Atom()认为它是一个原子,返回之;如果没有,则创建一个空白对象作为原子,返回。

Atom()并没有检查atom参数的有效性,但在这里QoBean强制约定“atom参数必须是一个对象实例”。之所以使用强制约定,而不是参数类型检查,这与QoBean在元语言上的基本思想有关:仅从元语言角度,QoBean认为JavaScript只有对象和函数两种类型,且函数也是一种对象。所以,对于Atom()来说,以下三种情况是合法的:

// 函数(包括构造器)可以作为atom  
a1 = Atom(function() {});  
// 对象实例可以作为atom  
a2 = Atom(new Object());  
// 也可以直接获取一个atom对象  
a3 = Atom();

当然有人会问:凭什么说一个函数从作为atom参数传入Atom(),再原封不动的传出来,就成了一个“原子”了呢?答案是:JavaScript固有的特性,任何两个对象既不相等(==),也不全等(===)。

也就是说,以JavaScript的固有性质来说,任何一个对象实例其实就是一个原子。即使任意两个空白的对象直接量,也是互不相等的。即:

alert( {} == {} ); // 显示false

当然,由于任何函数其实都是Function()的实例,所以也具有相同的性质:

// 显示true, 函数作为对象,是Function()的实例  
function foo() {};  
alert(foo instanceof Function);  
// 显示false, 函数都以原子对象的形式存在,故而互不相等  
alert((function(){}) == (function(){}));

二、原子的应用(1):识别器

在《JavaScript语言精髓与编程实践》中,谈到过对象的原子性的一种作用,亦即是作为“识别器”。例如说,我们知道硬币有正反两面,所以我们可以写这样的一个对象:

x = {
  front: true,    
  back: true    
}

显然我们可以用('front' in x)或者(x['front'])来识别它。但是这就存在了一个问题,因为x对象本来就有toString这样的属性,所以是不是说('toString' in x)为true,因此就表明x有一个名为'toString'的面了呢?同理,如果有人为x添加了一'middle'属性,那么将无法检查是x原本就有middle这个面呢,还是被某些代码污染了?

显然,按照对象设计的原理来说,将”属性”暴露出来,并可以任意读写,是导致这一切的根源。解决它的法子也很简单:用特定的方法来读取之。例如我们要查询"有没有某个面":

x = {  
  front: true,  
  back: true,  
  query: function(side) { reutnr side in this }  
}

我们跨出了正确的一步。但是,与此前完全相同的原因,我们调用x.query('toString')时仍然返回true。这显然不是我们想要的,因为硬币显然没有'toString'这样的'面'。

好吧,我们再向前行一步。我们知道任何一个对象都具有原子性,也就是说唯一。我们只要创建一个对象,让他成为query所需要的一个“钥匙”;然后把这个钥匙藏起来,这样谁也找不到它,于是谁也不能干扰这个对象所约束的那些性质了。

实现起来也很简单:

var coin = function() {  
  var exist = Atom();  
  var sides = {  
    front: exist,  
    back: exist  
  }  
  return {  
    query: function(side) { return sides[side] === exist }  
  }  
}();

好了,就这样。现在如果你调用coin.query('front')就一定返回true,而qoin.query('toString')则返回false了。

不过马上就会有人跳起来了:你这不是多此一举吗?既然sides已经是私有的了,就不必担心外面随意添加成员了呵!再则,不使用exist而使用类似true之类的值不也能判断吗?

是的,初看起来上述的置疑都对。不过在复杂的系统环境中,会存在三个问题,一个是sides[side]的效率不错,总比用数组实现来得强;第二个是,如果sides不在当前的函数内呢?第三个问题则更麻烦,如果你使用true之类的值,又如何避免第三方的代码通过Object.prototype来添加一个成员呢?

例如说,假定query()使用sides[side] === true来检测,的确可以避免toString之类的影响,但是如果有人写一行代码"Object.prototype.xxx = true",那coin.query('xxx')就将让人傻眼了。

所以,我们最好是找把钥匙藏起来,藏得好好的,别人都看不见。

三、原子的应用(2):友类与友函数

JavaScript没有明确的“类”的概念,所以这里讨论的友类与友函数其实是同一回事,只是Atom()作用于构造器还是普通函数的区别罢了。此外再强调一点,这里我们讨论的“友元”与“友函数”在名称上的确借鉴自C++,但概念上却有相当的差异。唯一与之相同的约定是,如果A是B的友函数,则A就能访问B的内部结构(例如私有成员)。

要实现这一点,我们得用Atom()把这两个函数联接起来。

举个例子来说,函数A内部有一个列表,记录了x,y,z三种状态。我们设定,有且只有函数B能修改之(当然A的一些内部的方法也能修改,但不是我们这里的主要问题),那么怎么办呢?

function A() {  
  var state = {  
    x: 100, y: 1000, z:5,  
    set: function(n, v) { this[n] = v }  
  }  
  // ...  
}  
function B() {  
  // 如何在这里修改A中的state?  
}

很自然的想法是让A公布一个方法出来。例如:

function A() {  
  var state = ...  
  // 公布一个方法   
  this.set = function(n, v) {  
    state.set(n, v);  
  }  
}

这样可以通过构建一个对象并用obj.set()来修改。或者我们将A整个的放在一个闭包里,再返回一个函数来做"修改state"的事情。但无论如何,我们只能做到“修改state",而无法做到“只有B能修改,其它位置都无法调用修改state的代码”。

好吧。传统观念上的、终极的方法, 是我们将state从A()里面移出来。然后将A()与B()放在同一个闭包里面:

function() {  
  var state = { .... };  
  function A() { ... };  
  function B() {  
    // 这样就保证仅有A与B是可以修改state的了。  
  }  
}

问题是,在组织大型的代码、类库或类继承树时,我们可能无法保证A与B处于同一个函数,或者他们根本就不是一个人写的,或者B会是将来的开发人员追加编写的……等等如此,反正A不见得总能与B在一个函数上下文里。

解决这个问题的方法呢?使用Atom()来为A()与B()建立一个友元关系,使它们成为友函数(如果在对象继承中,则可以实现为友类)。如下:

A = B = Atom();  
A = function(atom) {  
  return function() {  
    var state = { ... };  
    if (arguments[0]===atom) return state;  
    // ... 后续逻辑  
  }  
}(A);  
B = function(atom) {  
  return function() {  
    var A_state = A(atom);  
    // 后续逻辑  
  }  
}(B);

现在,在最初的时候,A和B都指向一个原子。在得到“真实的A()、B()"的同时,函数A()、B()各持有了atom的一个引用。此后,系统中就再也找不到atom了——没有任何方法可以A、B之外再他们所使用的atom。

由于A()与B()各持有了一个atom,于是,当B()函数调用A(atom)时,A函数就绝对可以信任这是来自于B的调用,因此将state返回即可——当然也可以返回存取函数,或者别的什么东西。对于B()的调用A(atom),以及A()对arguments[0]进行的识别,一切都是安全的、不可能受外界其它任何因素影响的。

四、其它

1、同样的方法,我们可以对更多个的函数、构造器或子系统(使用同一个闭包上下文的大段代码块)建立友元关系。

2、原子性是JavaScript对象的固有特性,使用Atom()函数主要是可以上述技巧在系统中具有明确的语义,这比随处定义一个"{}"来得要好。

3、QoBean内部使用这一技术来构建类继承关系,从而使子类可以访问父类的特性,对非子类来说则完全隔离。