前端要给力之:原子,与原子联结的友类、友函数
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内部使用这一技术来构建类继承关系,从而使子类可以访问父类的特性,对非子类来说则完全隔离。