Qomolangma实现篇(六):Qomo的OOP框架的实现技术

注:本文讲述的是FT4的一个修正版本中的OOP实现,而非原始发布版本。

一、Qomolangma中类继承的基本架构

Qomo在Object.js中,通过实现Class(),封装了“类继承”体系的绝大部分细节。如果你仅是要使用Qomo,那么你只需要通过“实现篇(五)”去了解一些基本语法就可以了。但是如果你想了解一些更细节的内容,或者你想让自己具有控制Qomo框架的能力,那么你应该继续将这篇技术文档读下去。

——尽管,这并不如你想象地那样容易。

Qomo的加入,使JavaScript具有了完整的“类继承”体系。类继承的出现,使JavaScript有能力处理更复杂的OOP语法和语义。而且能够通过更好的继承体系,来为JavaScript提供一些框架一级的语言特性,例如Interface与SOA。而这些能力,都会通过一个Class()关键字来实现。它内部实现的框架代码如下:

Class = function() {
  // ...
  // 基本的类数据、公共变量等

  // 构建类数据块
  function ClassDataBlock() {
    var cls = function (Constructor) {
      // ...
    }
    return cls;
  }

  // 真正的 Class() 函数
  function _Class(Parent, Name) {
    var cls = new ClassDataBlock(Parent, Name);
    cls.OnClassInitializtion(Constructor);

    // 构建对象(实例)数据块
    function InstanceDataBlock() {
      // ..
    }

    // 真正的对象构造器函数
    cls.Create = function () {
        // ...
 var Data = new InstanceDataBlock();
        this.get = Data.get;
        this.set = Data.set;
        this.inherited = Data.inherited;
        // ...

 if (this.Create) this.Create.apply(this, arguments);
        // ...
    }
    cls(Constructor);
    cls.OnClassInitialized(InstanceDataBlock);
    // ...

    // 替换构造器
    eval(Name + '= cls.Create');
    return cls;
  }

  return _Class;
}();

1. 类数据块(CDB)与实例数据块(IDB)

这是在Qomo中两个最重要的数据结构。籍由JavaScript中严格的上下文环境带来的特性,Qomo为每个类封装了一个类数据块(CDB, Class Data Block)。同时,也为每一个构造的对象封装了一个实例数据块(IDB, Instance Data Block)。

在上面的框架代码中,我们看到CDB与IDB都是通过new()关键字来创建的。不过,不同的是,InstanceDataBlock()是一个普通的构造函数,它返回由JavaScript的new()关键字创建的对象实例Data。而ClassDataBlock()并不这样,它内部暂存了new()创建的对象实例(this),而将一个函数对象cls返回给外部。

而我们看到,在_Class()中调用ClassDataBlock(),并将返回的函数对象cls作为结果值返回给外部。也就是说,如果我们用下面的代码:

function MyObject() {
}
TMyObject = Class(TObject, 'MyObject');

那么我们最后得到的TMyObject就是由ClassDataBlock()返回的cls。——而并不是真实的CDB数据块,真正的CDB被藏在了_Class()调用上下文环境的内部。

由于我们允许用下面两种代码来创建一个实例:

var obj1 = new MyObject();
// 或
var obj2 = TMyObject.Create();

而在框架代码的内部,我们看到下面的代码:

    // 替换构造器
    eval(Name + '= cls.Create');

相对于MyObject()来说,它事实上执行的是:

    eval('MyObject = cls.Create');

也就是说,前面提到的两种创建对象实例的代码,最终都将调用到cls.Create。也就是TMyObject.Create()。

我们也看到,在TMyObject.Create中,Qomo为实例创建了一个IDB:

    cls.Create = function () {
        // ...
 var Data = new InstanceDataBlock();
        // ...

而现在,通过这种封装结构,Qomo具有了一些特性:

  • 每一个类都有一个CDB。
  • 类的各个实例都有各自的IDB,但统一拥有类的CDB。
  • 在类的外部不能直接访问到CDB。同样,在实例的外部也不能直接访问到IDB。

2. “类”如何在构造期访问CDB

对于OOP系统来说,“类”通常是声明性的。也就是说,类一旦被声明,则类基本上只是代表一种特定的数据结构的“类型”。在Qomo当然也遵循这一原则。但Qomo不可能在JavaScript中“创建”一种自己的语法。——事实上,Qomo是不想创建这样一种语法,并用一个parser去解析它,然后要求开发人员重新理解、并在此基础上编程。

因此,Qomo采用了一些带有“声明性语义”的函数,或者符合JS规范的代码来完成这件事。例如:

// 详细参见“实现篇(五)——类声明周期与对象构造周期”
function MyObject() {
  Attribute(this, 'Value', 0);// 快速特性声明
  var v = _get('Data');       // 取(当前类的)父类的特性值
  _set('Data', v);            // 置特性值, 但不覆盖父类

  // ...
}

这里最重要的几个扩展函数就是_cls, _get, _set和Attribute()。在这几个函数的实现代码中,我们都可以看到:

_get = function(n) { return _get.caller.caller.get(n) }
_set = function(n,v) { return _set.caller.caller.set(n, v) }
_cls = function() { return _cls.caller.caller }

Attribute = function() {
  // ...
    var i, argn=arguments.length;
    var Constructor = Attribute.caller;
    var cls = Constructor.caller;
}

也就是说,它们通过“函数被调用时的上下文”来查找到“类引用(cls)”和“真实的构造函数(Constructor)”。这是因为这个Constructor总是在下面这样的环境中被调用的:

    var Constructor = eval(Name);
    // ...

    var cls = function (Constructor) {
      // ...
      base=new Constructor();
    }

    cls.OnClassInitializtion(Constructor);
    cls(Constructor);
    cls.OnClassInitialized(InstanceDataBlock);

而所谓Constructor,则是用户原始的构造器函数(而不是后来被替换的cls.Create)。因此,如下面的代码:

function MyObject() {
  Attribute(this, 'Value', 0);// 快速特性声明
  var v = _get('Data');       // 取(当前类的)父类的特性值
  _set('Data', v);            // 置特性值, 但不覆盖父类

  // ...
}
TMyObject = Class(TObject, 'MyObject');

在这个类的构造周期中,Attribute()调用中的caller就是Constructor(亦即是MyObject),而Constructor.caller就指向cls()这个函数,这就是Attribute()中下面代码的来源(其它几个函数类同):

    var Constructor = Attribute.caller;
    var cls = Constructor.caller;

而cls()调用发生在一个类初始化(OnClassInitializtion)和结束化(OnClassInitialized)之间。Qomo在初始化中写着:

    cls.OnClassInitializtion = function(Constructor) {
      if (Parent) Constructor.prototype = getPrototype(Parent);
      this.all = all;
      this.map = getInheritedMap;
      this.get = getAttribute;
      this.set = setAttribute;
      this.attrAdapter = getAttribute;
    }

这样,cls.get()、cls.set()等特性在“类构造周期”就可用了。而接下来:

    cls.OnClassInitializtion = function(Constructor) {
      delete this.all;
      // more ...
    }

又把这些属性和方法给清除掉。使得“类构造周期”之外不可能再通过这些方法来访问CDB。

3. “对象(实例)”如何访问IDB

对象实例(自身)的构造过程其实是由new ()关键字完成的。如下面的代码:

var obj2 = TMyObject.Create();

这时进入Create()方法时,this指向TMyObject自身。因此_Class()中下面的代码被调用:

    cls.Create = function () {
      if (this===cls) {
        // 'this' is class ref.
        var i, v=arguments, n=v.length, s='new this.Create(';
        if (n>0) for (i=1,s+='v[0]'; i<n; i++) s += ', v[' + i +']';
        return eval(s+');');
      }
      // ...

这段代码实际上是重新调用new this.Create(),并传入参数。而我们前面提到过_Class()将会把构造器替换成cls.Create()。因此这段代码就达到了使下面两种语法等义的效果:

var obj1 = new MyObject();
// 或
var obj2 = TMyObject.Create(); // 实际将调用上一种语法

而同样的道理,new MyObject()其真实的调用会是“new cls.Create()”,这种情况下,在Create()函数中,this对象将指向新创建的实例,且this.constructor === cls.Create。所以将会调用到下面的代码:

    cls.Create = function () {
      // ...
      else if (this && this.constructor===cls.Create) {
        // Make a DataBlock for per Instance, and reset attributes getter/setter.
        var Data = new InstanceDataBlock();
        this.get = Data.get;
        this.set = Data.set;
        this.inherited = Data.inherited;
      // ...

这样一来,新的对象实例(this)将会执有cls.Create()的一个上下文,并通过创建一个IDB的实例Data。然后,通过"this.get = Data.get"这样的代码,使对象实例执有一些访问IDB内部私有数据的方法。这样一来,我们在外部代码中就可以get/set/inherited,但却无法存取到IDB(也就是私有变量Data)中的数据了:

var obj = new MyObject();

// 下面的代码将实际存取Data中的私有数据
obj.set('Value', 100);

4. 对象构造周期:如何统一原型构造与类构造体系

有两种方法来实现“类构造体系”。其一是用类抄写,也就是试图用下面这样的代码:

var clrRef = TObject;
var obj = New TMyObejct();

for (var i in clrRef) obj[i] = clrRef[i];

我们的确可以通过这种方法来使得obj拥有一份TObject中的对象方法、属性与数据存取界面。——事实上这也是Qomo的前身WEUI采用的方法。——但是这样实现的效率是极低的。

在Qomo中,采用了第二种方法,也就是通过“原型继承”来实现“类继承”。在这样的方案中,开发人员看到的会是“类继承”体系中的代码,而Class()关键字则在注册过程中隐含地完成了原型链的维护:

  function setClassTypeinfo(cls, Attr, instance) {
    // ...
    cls.Create.prototype = instance;
  }

  function ClassDataBlock()
    cls.OnClassInitializtion = function(Constructor) {
      if (Parent) Constructor.prototype = getPrototype(Parent);
      // ...
    }

    var cls = function (Constructor) {
      // ...
      setClassTypeinfo(cls, Attr=new Attr(), base=new Constructor());
    }

    returtn cls;
  }

  function _Class() {
    var cls = new ClassDataBlock(Parent, Name);
    cls.OnClassInitializtion(Constructor);

    cls.Create = function () {  /* ... */ }
    cls(Constructor);

    eval(Name + '= cls.Create');
    return cls;
  }

我们看到,“类注册Class()”调用的本质,是:

  • 创建CDB,并生成类的原型base. 这个原型是通过用户的构造函数Constructor来创建的,它的原型指向父类的原型:getPrototype(Parent)
  • 对象构造函数指向cls.Create(),而在cls()调用过程中将setClassTypeinfo()。这个过程传入的instance来自于类的原型,也就是"base=new Constructor()"。这意味着“类只是声明并实现了原型”,而对象“创建自类的一个(原型)复制”。

使用这种技术达到的效果,与前面提到的“类抄写”是一致的。但由于利用了系统内置的原型机制、写复制机制和继承关系,因此效率上将高许多。而且由于“对象构造”其实基于一个“类实例的原型”,因此下面的代码就将“对象构造周期”与“类构造周期”分离开来了:

  function _Class() {
    // ...
    cls.Create = function () {
        // ...

 // 如果有Create(), 则调用以启动“对象构造周期”
        if (this.Create) this.Create.apply(this, arguments);
    }
  }

function MyObject() {
  // 下面的代码使类原型得到了Create()方法, 它被理解为对象构造周期的入口
  this.Create = function() {
    // 在下面的代码中的this即是对象实例
  }
}
TMyObject = Class(TObject, 'MyObject');
var obj = new MyObject();

二、Qomolangma中的特性(Attribute)系统

在充分意识到原型继承的优点之后,Qomo在FT4之后的代码中,采用了类似实现“类继承”机制的代码来实现了特性(Attribute)系统。——而在FT4发布的Object.js代码中,采用的则是类似于“类抄写”的机制。

Qomo为每一个类保存了一个类引用,这被放在一个名为"Class Type Info"的结构中,

  function ClassTypeinfo(cls, Attr) {
    this.class_ = cls;
    this.$Attr_ = Attr;
    this.next__ = _classinfo_[cls.ClassName];
  }

全局对象_classinfo_用于保存所有类信息的入口,如果两个类名一致,则该入口被理解为一个链表。——这用于处理不同命名空间上的同名类。——而_classinfo_只是Class()上下文的全局可见,在Global全局则不可见。这避免了外部的代码修改它。

在Qomo的类系统中,构建“类类型信息”时,采用的是如下的代码:

  function setClassTypeinfo(cls, Attr, instance) {
    // ...
    // 查找类信息入口并处理重复注册问题

    _classinfo_[n] = new ClassTypeinfo(cls, Attr);
  }

  function ClassDataBlock() {
    var Attr = function() {}; // all getter and setter method of attributes

    var cls = function (Constructor) {
      var base, parent = getPrototype(cls.ClassParent);
      if (cls.ClassParent) Attr.prototype = getAttrPrototype(cls.ClassParent);
      setClassTypeinfo(cls, Attr=new Attr(), base=new Constructor());
      // ...
    }

正是这些代码展示了Qomo如果驾驭原型继承机制来实现复杂的Attribute系统。

1. Attribute系统的构建

Attr首先是一个空的函数。准确地说,它是一个原型系统中的构造器。接下来,cls()函数中检测如果cls存在父类ClassParent,则通过getAttrPrototype()获得父类的Attriburte原型,并置为构造器Attr的原型“Attr.prototype”。

接下来的使用Attr()来构造原型,并替换掉这个不现使用的构造器。因而Attr=new Attr()执行之后,Attr将是一个有效对象实例。它携带了所有从父类的Attribute中“原型继承”得来的读写器(getter/setter),以及特性值(AttributeValue)。

接下来,“base = new Constructor()”这行代码开始调用。由于在Constructor中可以添加新的读写器,以及声明Attribute。例如:

// 用户的Constructor
function MyObject() {
  this.getData = function() { /* ... */ }
  Attribute(this, 'Width', 100, 'rw');

  // ...
}

这样一来,返回的base对象将与父类的原型存在差异。差异中如果有Attribute,那么应该当记入Attr对象。因此就有了下面的代码:

    var cls = function (Constructor) {
      // ...

      for (var i in base) {
          // ...
   if (_r_attribute.exec(i)) {
            Attr[i] = base[i], delete base[i];
            if (!(RegExp.$2 in Attr)) Attr[RegExp.$2] = undefined;
          }
      }
    }

这段代码用于分析base中的属性i是否是getter/setter,如果是,则在Attr上保存一个引用,并从base(类原型)上删除这个属性。——而后,(如果需要)再在Attr上建立一个与特性同名的属性,用于存值。

相对于上面的MyObject(),这段cls()代码的处理使得该类拥有如下的一个Attr:

// 等效于如下声明
Attr = {
  Width: 100,
  Data: undefined,
  getData: function() { /* ... */ }
}

而这个Attr被记入到typeinfo。对其子类来说,就可以通过getAttrPrototype()来获取并作为自己的原型,从而创建起Attribute的原型继承链。

2. Attribute的读写

对于用户代码来说,读写Attribute的其实是对象实例。因此,这个读/写方法应该是对象的方法。而刚才我们看到Attr其实是在ClassDataBlock内部,也就是类的内部、对象的外部。这意味着我们不能在对象中直接存取到它。

Qomo通过类的OnClassInitializtion/OnClassInitialized,来打通了“对象->类”访问的通道。具体的策略,就是在“对象可访问的上下文中,保存了三个对象方法的指针”:

  function _Class(Parent, Name) {
    // ...

    // some member reference for class
    var $all = cls.all;
    var $map = cls.map;
    var $attr = cls.attrAdapter;  // 保存给Attribute系统使用
    function InstanceDataBlock() {
      // ...
    }
  }

这样,在IDB中就可以通过$attr来访问对父类的Attr中的getter/setter()。——尽管在后面的代码中将使得这是不必须的,但Qomo中还是保留了这一做法。

接下来,当类完成初始化时:

  function ClassDataBlock() {
    cls.OnClassInitialized = function(IDB) {
      // ...
      if (Parent) IDB.prototype = getAttrPrototype(cls);
    }
  }

  function _Class(Parent, Name) {
    // ...
    cls(Constructor);
    cls.OnClassInitialized(InstanceDataBlock);
  }

我们看到IDB的原型被置为了当前类(cls)的Attribute原型。这使得在对象构造期间,我们通过new InstanceDataBlock()得到的实例(this)将是与类的Attr原型等同的一个实例:

    function InstanceDataBlock() {
      var data_ = this;

      this.get = function (n) {
        //...
        // 处理不同的调用参数

        return data_[n]; // a value
      }
    }

因此obj.get(n)就可以简单地返回data_[n]。当然也可以快速地处理到Attr中保存过的getXXXXX()函数。——如果它存在的话:

      this.get = function (n) {
        //...
        // 处理不同的调用参数

        else {
          // get custom-built getter from cls's $Attr.getXXXXXX
          // in ClassDataBlock, the ref. equ data_['get'+n]
          var f = $attr('get'+n);
          if (f) return f.call(this, n);
        }
      }

3. “类构造周期”中的Attribute读写

“类构造周期”中对Attribute的读写与对象实例的读取稍有差异,但本质上是一致的。因为类中Attr对象既是子类的Attr的原型,也是一个私有的对象。因此:

    function getAttribute(n) { return Attr[n] }
    function setAttribute(n, v) { Attr[n] = v }

    cls.OnClassInitializtion = function(Constructor) {
      // ...
      this.get = getAttribute;
      this.set = setAttribute;
    }

类引用(this)上的get/set,是留给“类构造周期”中的_set()/_get()和Attribute()来使用的。例如_set的代码就是"_set.caller.caller.set(n, v)"。

三、Qomolangma中如何用inherited来调用父类方法

obj.inherited()试图让当前对象具有访问父类方法的能力。这种能力应该是内置于系统的,而不是象一些其它OOP框架那样,要求写如下的代码:

this.method = function() {
  this._parent.method.apply(this, arguments);

  // do other..
}

尽管inherited()的含义,的确与上面的代码达到的效果一致。但如果让开发人员都这样写代码却实在有点勉强,况且还需要在对象系统中再维护_parent。——当然,在事实上开发人员也可以通过this.constructor.prototype来得到它,而这样处理也更加麻烦。

1. inherited的实质是父类原型的遍历

Qomo试图把复杂的事物变得简单一些。尽管在这些简单的背后,是和原始情况一样、甚至有过之的复杂。Qomo的obj.inherited()的实现中最核心的一行代码是这样:

        var p = cache[cache.length] = $map.call(this, f).slice(1);

其中$map来自于父类的CDB中getInheritedMap()函数的一个引用:

  function ClassDataBlock() {
    // ...

    // 在CDB中,在类初始化阶段公布getInheritedMap
    cls.OnClassInitializtion = function(Constructor) {
      // ...
      this.map = getInheritedMap;
    }
  }

    // 获取getInheritedMap()的一个引用
    var $map = cls.map;

    function InstanceDataBlock() {
      // ...
      this.inherited = function(method){
        // 调用 $map()
      }
    }

而getInheritedMap用于从当前类及其原型中查找指定方法的call map。这个map就是一个栈,用于收集在当前类及父类中所有被覆盖(override)的方法。栈顶是调用inherited()方法时的这个函数。

在类及父类中获取call map的代码并不复杂。我们遍历所有的父类prototype,然后通过调用检测方法/属性的函数hasOwnProperty(),来判定该方法是否被修改过(一旦该方法被覆盖,那么hasOwnProperty()的返回结果将是true):

  var n=getPropertyName(method, this);
  // ...

  // 由于处于CDB, 所以能直接访问cls引用
  $cls = cls;
  while ($cls) {
    p = getPrototype($cls);
    if (p.hasOwnProperty(n)) a.push(p[n]);
    $cls = $cls.ClassParent;
  }

上面的代码中getPrototype()是在Class()所保留的_classinfo_中,通过类名来查找它的原型引用。

然而我们也发现一个问题,即使通过name字符串在_classinfo_效率不错,但我们也需要通过不断的原型遍历来查找整个的call map。因此如果每次obj.inherited()都要重复这个过程的话,那么效率将会很差。

因此,Qomo为每一个call map建立了一个缓存。它基于两个规则:

  • 即使是大型系统中,inherited()的使用也不是对每个方法的,也就是需要inherited()
    的方法数小于对象方法数。
  • 如果A方法试图inherited(),那么它每次调用inherited()总会得到相同的方法引用。

可见每次都通过getPropertyName()来得到name,并通过name访问cache的方法并不可取。因为getPropertyName()本身就需要遍历一次对象方法和属性。因此Qomo建立的call map缓存的第一个结点并不是PropertyName,而是当前调用inherited()的方法本身:

this.inherited = function(method){
  var f=this.inherited.caller, args=f.arguments;
  // ...

  // find f() in cache, and get inherited method p()
  for (var p, i=0; i<cache.length; i++) {
    // 查找cache,并检测map[0]是不是指定的方法f()
    if (f === cache[i][0]) {
      p = cache[i];
      p.shift();                      // 移除当前方法
      return p[0].apply(this, args);  // 调用父类方法
    }
  }
}

2. 同一对象方法中,多次inherited()调用的实质是call map栈的出栈

上面提到了call map是一个栈,它的建立是通过getInheritedMap()对父类原型进行遍历。而inherited()通过cache[i][0]查找到当前正在调用的函数和call map。

这样一来,在A去inherited(B)时,B如果又inherited(D),那么D调用obj.inherited(),仍然会回到这个函数,并见到cache[i][0]上的方法D。这时,call map的下一个方法就可以被取出来,并执"p[0].apply(this, args)"操作。

很明显,如果有多次的inherited(),那么整个的行为看起来就象是cache[i]中的元素在出栈。

问题是:如果到达最后一个元素呢?

根据inherited()的语义,我们的确有可能在一个类的方法中试图去inherited()一个父类中不存在的方法。——你或者写错了类,或者是开发人员用错了inherited(),总之这是必将会发生的一件事。

Qomo为此做好了准备。Qomo在构建call map的时候,为栈底(数组最末元素)填入了一个名为$inherited_invalid的方法。这个方法将触发一个异常。

而如果一个方法在父类中根本没有同名的(被覆盖的)方法,那么在cache中它将有一个如下格式的call map:

a = [method, $inherited_invalid];

// 该call map被填入_maps末尾
_maps[len] = a;

很显然,如果要对这个method进行inherited(),那么将立即执行到$inherited_invalid()并导致一个异常的触发。

3. 在Attribute的读写器中的inherited()

相对于Attributes系统,Inherited实现起来更为复杂。其中的因素之一,就是Inherited事实上也需要实现一部分Attributes的功能。例如:

function MyObject() {
  this.getData = function() {
    return 100
  }
}

function MyObjectEx() {
  this.getData = function() {
    return this.inherited() * 2
  }
}

TMyObject = Class(TObject, 'MyObject');
TMyObjectEx = Class(TMyObject, 'MyObjectEx');

var obj = new MyObjectEx();
alert(obj.get('Data'));

在这个例子中,我们见到读写器方法getData()也需要调用父类方法。而我们也知道,“父类方法MyObject.getData()是被Attributes系统隐藏的,因此如果要Inhterited()访问到,就需要Attribute提供相应的机制。

但这在Qomo中并不麻烦。因为建立call map的getInheritedMap()运行在CDB的上下文中,因此它可以访问内部的Attr对象的属性和原型。所以getInheritedMap()中的实现代码并不复杂:

function inheritedAttribute(foo) {
  // ...
  var p, v=[], $cls=Parent;
  while ($cls) {
    p = getAttrPrototype($cls);
    if (p.hasOwnProperty(n)) v.push(p[n]);
    $cls = $cls.ClassParent;
  }
  if (v[0] !== foo) v.unshift(foo);
  return v;
}

function getInheritedMap(method) {
  //...
  var a=inheritedAttribute(method);
  // ...

  a.push($inherited_invalid);
  return (_maps[len] = a);
}

四、Qomolangma中的一些问题

事实上我们已经发现,Qomo为IE 5.0兼容做出了太多的牺牲。例如在common_ie5.js中使用了大量的代码来提供与IE5.5+相等同的RegExp.replace(),也使用了较低效的方式来避让了IE 5中存在的RegExp.lastIndex的BUG。

即使如此,我们仍旧认为这些兼容的工作是值得的。然而现在,在Object.js之后,我们终于发现了无可回避的问题。这主要体现在两个方面:

  • <property> in <object> 的使用
  • apply与call的实现

1. <property> in <object> 的使用

在Object.js之前的代码中,都已经尽可能地避开了不能在IE 5.0中使用的<property> in <object>语法。然而我们也看到,使用下面的代码并不能真正的解决问题:

if (typeof object[property] == 'undefined') {
  // ...
}

这样的语句事实上并不能替代<property> in <object>。因为如果"obj[n]=undefined",
那么就将再也无法通过这段代码来完成检测。因此更有效的代码是:

function hasProperty(p, o) {
  for (var i in o) if (p==i) return true;
  return false;
}

然而我们不能要求开发人员为了兼容IE 5.0都使用hasProperty()来替代<p> in <o>的语法。因此需要在IE 5.0中引入一个parser,对$import()的.js文件中的代码进行替换。

然而这件工作变得异常坚巨。其中最重要的原因是:

  • 我们无法快速地识别for (p in o)if (p in o)之间的差异
  • 我们也无法对类似于if (!(o in o))这样多层的运算进行快速、有效的parser

因此<p> in <o>已经让我们在IE 5兼容的道路上遇到了大麻烦。但即使如此,这一切仍不是最重要的。因为Zhe(fangzhe@msn.com)已经开始尝试一个更强大的parser,用于解决Mozilla与safari上的一些BUG修补。如果他的工作能有一些进展,则<p> in <o>的问题仍然有望解决。

真正的问题出在call()与apply()。

2. apply与call的实现

在IE 5上实现apply与call并不难。其基本的做法是:

Function.prototype.apply = function(obj, args) {
  for (var i=0, arr=[]; i<args.length; i++) arr.push('args[' + i + ']');
  obj._func = this;

  return eval('obj._func(' +
    arr.join(',') +
    ')'
  )
}

// call()调用apply()并传入参数表
// Function.prototype.call = ...

这样的代码将得到很好的执行。事实上细节可以掩饰得更好,例如Qomo中就在eval()执行的字符串中加入了一行delete obj._func来处理掉这个多余的属性。

在apply()的实现中,采用eval()的真正原因是:只有eval()才能访问上下文中的变量obj和args,换做execScript()或者使用new Function()来创建函数并执行都不能取得这种效果。

然而eval()执行出现了另一个问题,也就是在eval()中调用obj._func()时,_func()上下文件中得到的caller为null:

function foo(v) {
  alert(foo.caller);
  alert(v);
}

function foo1() {
  var a = 100;
  eval('foo(a)');
}

foo1();

这对于一般的代码来说并没有什么影响。然而Qomo在实现inherited()的时候,以及实现obj.get/set方法时,使用了caller属性来取得调用者函数的引用。然后get/set和inherited过程中,将大量使用caller来处理多层调用间的关系。

这些技巧是使得Qomo有能力处理this.get()这种简略语法的关键。然而多层的调用却不得不通过apply()来传递参数。——显然“IE5上apply()中的代码得不到caller”已经成为致命的问题了。

鉴于此,Qomo从FT4开始将暂停对IE 5.0的兼容性支持。