日期:2014-05-16  浏览次数:20318 次

深入理解Javascript
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整个的放在一个闭包里,再返回一个函数来做"修改