一、Qomolangma中完整的OOP支持:Object.js
Qomo自Field Test4开始提供Object.js,这个单元用于在Qomo中支持完整的OOP特性。通过Object.js,Qomo中的JavaScript由原来的“原型继承”转变成了“类继承”。而且,这个转变对于开发人员来说,几乎是完全透明的。
通过Object.js,Qomo支持了如下的面向对象特性:
- 特性(Attribute)及读写器(getter/setter)
- 类注册与类继承(Class),以及类在命名空间上的注册
- (继承)父类的方法调用(Inherited)
(TODO: 在safari中,由于不支持function.caller属性,因此无法支持Qomo中的Object.js)
二、Qomolangma中的OOP语法
Qomo的确扩展了一些JS语法。但与一些其它的JS OOP实现方案不同:Qomo是基于JS语法自身扩展,而不是加上一些替换用的标识/关键字。后者的实现方案,可能要求单独的一个语法翻译系统/解释引擎。但Qomo不需要,而且基本上来说,由于不存在syntax parse,所以Qomo运行的速度要较快,而且开发人员书写的代码与调试器中的代码完全一致。
Qomo在全局提供了三个以关键字形式描述的函数:Class(), Attribute()和Abstract()。其中Class和Abstract事实上使用了js保留的关键字class与abstract。这意味着在今后的(例如Qomo for JS 2.0中)Qomo有可能删除掉它们。
这些函数中,Class()用于将一个构造器函数注册到一个类; Attribute()用于快速地声明和初始化特性; Abstract()用于声明一个抽象方法。
Qomo在全局提供了三个仅能在类注册和初始化阶段使用的函数:_set(),_get()和_cls()。它们用_
开始,表明调用它们的上下文环境是受限的:只能在类声明过程中被调用。
在Object.js中,还重写了Object()类,重写的Object()与JavaScript原有的Object()具有完全相同的特性。事实上,它的原型就是原来JavaScript Object()的一个实例。
重写Object()而不是直接使用原有的,是因为在Qomo中有一项重要约定:
- Qomo中应该允许自由地使用"{}"语法来声明直接量的对象,而且这个对象一定是没有任何“可见的”属性的。
由于Qomo是使用“类继承”体系的,因此Qomo注册了一个全局的基类:TObject。
1. 类声明与类注册(Class)
Qomo中声明一个类的方法,与标准JS中声明一个“对象构造器(函数)”的方法完全一致。但是,在声明类之后,Qomo需要调用Class()来注册它。如下例:
// 类声明
function MyObject() {
this.value = 'Hello, Qomoer!!!';
}
// 类注册
TMyObject = Class(TObject, 'MyObject');
// 创建实例和调用
var obj = new MyObject();
alert(obj.value);
我们也注意到,除了多一行类注册之外,Qomo其它的(基本)语法与标准的JS没有任何的不同。
但是,通过Class()注册,得到的对象实例obj将会具有以下三个“多余的”方法调用:
- obj.get() : 取特性值
- obj.set() : 置特性值
- obj.inherited() : 继承(调用)父类方法
关于Qomo的类与对象实例的一些特征,请参见示例:TestCase/BaseObjectDemo.html
接下来我们讲述与这三个方法相关的一些语法。
2. 特性(Attribute)的使用
在Delphi中属性是可以有读写器的,在C#等其它高级语言中也可以具备这些。包括在JS2.0中,也提供了属性读写器的声明语法。但这些都是基于属性(properties)的。
Qomo没有办法提供一种“扩展的语法”来使得JS 1.3支持“属性的读写器”。因此Qomo借用了C#中"特性(Attribute)"一词,实现了可定制读写器的Attribute。事实上,也如同C#所推荐的一样:Attribute是“最具创新的一种构造”。
Qomo中可以在“类声明”中直接声明特性(注意这些特性声明/方法不会出现在对象实例的属性中,即使是使用for..in运算来列举),而无需事先描述它:
function MyObject() {
this.getValue = function() {
return 100;
}
}
TMyObject = Class(TObject, 'MyObject');
var obj = new MyObject();
alert(obj.get('Value'));
简单地说,Qomo认为所有使用'get/set'开始的方法,都是一个指定特性的“读写器方法”。因此一行简单的“this.getValue = function(){...}”,就具有了以下语义:
- (在对象实例内部, )声明一个名为"Value"的特性
- 它的读方法(getter)为getValue()
- 它的写方法(setter)将直接操作"Value"特性本身
在上面这个例子中,我们看到getValue()将直接返回100。那么如何能返回“对象实例内部的Value特性”的值呢?——因为setter没有被声明,会直接操作这个内部值。参见这个示例:
function MyObject() {
this.getValue = function() {
return this.get();
}
}
TMyObject = Class(TObject, 'MyObject');
var obj = new MyObject();
obj.set('Value', 1000);
alert(obj.get('Value'));
这个例子表明,“(仅仅)在读写器函数内部”,可以不带"name"参数地调用:
- this.get() : 用于取内部特性的数据
- this.set() : 用于置内部特性数据的值
注意这两种调用总是发生在读写器方法的内部,这时this总是指向当前实例。此外,对于“写方法”来说,要多一个参数。例如:
function MyObject() {
this.setValue = function(v) {
return this.set(v*2);
}
}
TMyObject = Class(TObject, 'MyObject');
var obj = new MyObject();
obj.set('Value', 1000)
alert(obj.get('Value'));
在内部实现上,Qomo的特性采用了“写复制”的技术,因此一个类的所有实例的特性初值是一样的。但如果某一实例重写了它,则该实例具有一个不同的值。此外,子类对象实例也共享父类特性的一份引用(而不是拷贝)。但当实例试图写特性值时,它将写在自己的数据区写,而不会影响类或者其它实例。
特性初值由Attribute()函数来声明,它仅能在类声明阶段被调用。例如:
function MyObject() {
Attribute(this, 'Value', 100, 'rw');
}
TMyObject = Class(TObject, 'MyObject');
var obj = new MyObject();
alert(obj.get('Value'));
Attribute()的参数说明如下:
- base : 必须是this
- name : 指定特性名. 习惯上以大写开头.
- value: 初值
- tag : 标志字符. 目前仅用于描述读写特性. 包括字符:r, w
Attribute()的tag参数(目前)用于描述读写性。如果尝试读一个只写的特性时,将触发一个异常,反之亦然。但是,在读写器方法的内部就没有这项限制。例如:
function MyObject() {
Attribute(this, 'Value', 100, 'r');
this.getValue = function() {
this.set(100);
return this.get();
}
}
TMyObject = Class(TObject, 'MyObject');
var obj = new MyObject();
alert(obj.get('Value'));
// 以下代码将导致一个异常
// obj.set('Value', 'hi, error!');
不过,Attribute()所声明的tag是非强制性的。也就是说,即使声明了只读/写特性,仍然可以通过一个指定名字的读写器方法来使“只读/写”失效。例如:
function MyObject() {
Attribute(this, 'Value', 100, 'r');
this.setValue = function(v) {
this.set(v);
}
}
TMyObject = Class(TObject, 'MyObject');
var obj = new MyObject();
obj.set('Value', 'hi, success!');
alert(obj.get('Value'));
这样设计的原因,是使得子类有机会重写父类的特性的读写器方法。——他们只是名字上的相同,但可读写性不一定要一致。例如:
function MyObject() {
Attribute(this, 'Value', 100, 'r');
}
TMyObject = Class(TObject, 'MyObject');
function MyObjectEx() {
this.setValue = function(v) {
this.set('value is: ' + v);
}
}
TMyObjectEx = Class(TMyObject, 'MyObjectEx');
var obj = new MyObjectEx();
alert(obj.get('Value')); // 显示自MyObject()继承来的特性值
obj.set('Value', 'hi, success!'); // 为实例特性置新值
alert(obj.get('Value')); // 显示新值
var obj2 = new MyObject();
alert(obj2.get('Value')); // 检测:其它的实例没有受到影响
// 对于MyObject()来说,Value特性是不可写的,因此下面的代码将出错
// (注: 在Field Test 4中,这项限制被暂时地取消了.)
// var obj3 = new MyObject();
// obj3.set('Value', 1000);
关于Attribute()的更多示例,请参见:TestCase/BaseObjectDemo2.html
3. 继承(Inherited)方法的使用
对于一个实例来说,obj.inherited()将调用父类方法,这种调用关系可以在父类的父类、乃至祖先类中发生。当调用到根类TObject()时,由于不存在同名的方法,因此将会返回一个异常。
在Qomo中,inherited具有与delphi中的inherited完全一致的特性。当然,它只能在方法中被调用。这包括类(声明)方法与对象方法。——这两者的区别随后再介绍。
inherited有三种调用方法(由于它总是在方法内被调用,因此this总是指向当前实例):
- this.inherited() :
- this.inherited(this.mehtod [, param]);
- this.inherited('
' [, param]);
第一种方法没有任何参数,这种情况下,Qomo将查找当前方法的父类方法,并使用相同的入口参数调用;对于第二、三两种方法来说,则可以传入一个新的参数表。如果参数忽略,也将使用当前方法的参数表。
第二、三两种方法本意上是相同的,只是一个是用方法名,另一个是用方法引用来作为第一个参数。二者不存在效率上的差异,只是用于应对不同的需求。
关于如何对象的继承性,需要读者去阅读有关OOP的相关书籍了,这里不细述。下面的例子描述如何在Qomo中使用这一特性:
function Animal() {
this.leg = function() {
alert('跑');
}
this.run = function() {
this.leg();
}
}
function Cat() {
this.jump = function() {
alert('跳');
}
this.run = function() {
this.jump();
this.jump();
this.inherited();
}
}
TAnimal = Class(TObject, 'Animal');
TCat = Class(TAnimal, 'Cat');
obj = new Cat();
obj.run();
这个例子说明:
- “动物”有一种run的行为,这个行为的内容是(不停地)“跑(leg)”
- “猫”是一种“动物”,并从“动物”那里继承了run的行为
- “猫”通常是“跳两下”,然后再继续run的行为
所以,猫的run的行为描述是:
this.run = function() {
this.jump();
this.jump();
this.inherited();
}
关于inherited()的更多示例,请参见:TestCase/BaseObjectDemo3.html
3. 类声明周期与对象构造周期
在此前的所有例子中,我都没有把Qomo中一项最重要的特性展现在用户面前。这就是“类声明周期”。——即使在绝大多数的例子前面,留有这样注释的。
Qomo中用户代码编写的“构造器”,其实是只应当具有“类声明语法”的。所谓类声明语法,就是指“仅于一个对象的表现有关”的语法。而不应该加入过多的逻辑代码。基本上来说,类声明可以包括如下代码:
function MyObject() {
Attribute(this, 'Value', 0);// 快速特性声明
var v = _get('Data'); // 取(当前类的)父类的特性值
_set('Data', v); // 置特性值, 但不覆盖父类
var cls = _cls(); // 取类引用
var count = 10; // 声明类(私有)静态成员
var foo = function() {}; // 声明类私有函数
function foo2() {}; // 同上
this.data = 123; // 公开属性
this.getValue = function(){ // 特性读方法
this.set(); // 内部的读方法
this.get(); // 内部的写方法
}
this.setValue = function(){ // 特性写方法
// ...
}
this.method1 = function() { // 类方法
this.inherited(); // 继承(调用)父类方法
}
// (other code...)
}
TMyObject = Class(TObject, 'MyObject');
上面的示例包括了“类声明周期”的绝大多数代码及其语法。当然,在"other code"中用户也可以加入自己的代码。例如初始化类的一些特性,或者将类加入全局的monitor等等。但用户需要注意的是,在“类声明周期”:
- this指向所有对象实例的原型
- 可以用_cls()来取得类自身的一个引用
- 不能直接操作当前类声明的特性值(可能会存在一些限制)
- 不能给(类声明时的)构造器加入口参数
- 这个构造器函数只被执行一次
对于用户代码来说,构造器不能加入口参数很大程度上的限制了使用。但事实上,在“类继承体系”中,这是很合理的。——你不能用同一个类声明去描述两个不同的类及实例。
在Qomo中,用户声明的“构造器”实际上被用作“类声明”。因此就独立出来一个“对象构造周期”。这个周期是用户代码完全可控的:
function MyObject() {
// (略: 类声明)
this.Create = function(v1, v2, v3) {
// 对象构造周期
}
}
TMyObject = Class(TObject, 'MyObject');
“对象构造周期”是由this.Create()来描述的一个函数。它也很象标准JS中的构造器:
- 每次对象构造都将为实例调用this.Create()方法
- 该方法可以有任意的入口参数
- 该方法中的this,是指向对象实例的
- 在该方法中,可以访问类的私有成员和方法
因此,用户可以按自己的习惯在这里书写任意的JavaScript代码。但是与“类声明相关的”一些语法不能在这里使用:
- Attribute(), _set(), _get(), _cls()不能使用
- this.getValue()等不能声明成特性
但有一个唯一的例外:this.inherited()可以在“对象构造周期”使用:
function MyObject() {
this.method1 = function() {
// ...
}
this.Create = function(v1, v2, v3) {
// 对象构造周期
// ...
if (!v1) return;
this.method1 = function() {
this.inherited();
}
}
}
TMyObject = Class(TObject, 'MyObject');
这个例子中,“对象构造周期”中的method1()事实上覆盖了“类.method1”方法。但它不会改变其它该类构造的其它实例(如果v1值是有效的)。因此“对象方法”与“类声明的方法”是被区分开来的。
因为这种区分,所以你应该知道“对象构造周期”如果覆盖了“类声明的方法(A)”,那么这个inherited将会调用“方法A”。——inhreited()的语义是“父类方法”。
这个inherited()的特性,请参见:TestCase/BaseObjectDemo3.html
三、Qomolangma中OOP语法的一些注意事项
首先,最重要的一点是:Qomo的多投事件系统对任何框架来说,是“完全透明”的!因此,它可以在其它任何框架中,象一个普通的事件函数(响应句柄)一样地加入被植入。
事实上,Qomo的多投事件与Qomo OOP框架完全地脱离开,不利用任何的OOP特性、框架特性。——这种设计思路完整地体现了Qomo的目标与宗旨,以及,我们对OOP的认知。
1. 类声明与构造的一些特例与语法
Qomo中Class()可以只有一个参数,即Constructor Name
。这有两种情况:
// 语法一:值为'Object',仅仅保留给TObject的声明
function Object(){
}
TObject = Class('Object');
// 语法二:值不为'Object',相当于调用Class(TObject, '<Constructor Name>');
function MyObject(){
}
TMyObject = Class('MyObject');
``
此外,Class()还有两种可能的用法:
```javascript
// 语法三:Class()在声明前调用
TMyObject = Class(TObject, 'MyObject');
function MyObject(){
}
// 语法四:只注册但不声明类
function MyObject(){
}
Class(TObject, 'MyObject');
语法三使用了JavaScript编译期的特性。由于直接声明的function将在编译期被接受,因此TMyObject对'MyObject'的使用可以出现在它的声明之前。但是下面这样的用法就不行(关于这一点,在以前的《Qomolangma内核篇(四)》中有讲过):
// 不正确的用法
TMyObject = Class('MyObject');
MyObject = function(){
}
语法四利用了Qomo中不强制类声明的特性。这种用法只将MyObject()注册成为类,但不提供一个类类型TMyObject。这种情况下,仍然可以通过obj = new MyObject()来得到类实例。而与类类型TMyObject完全等义的引用,可以在以下两个地方找到:
obj = new MyObject();
// 1. 在obj.ClassInfo中存在类引用
alert(obj.ClassInfo.ClassName);
// 2. 在命名空间上存在该类引用
var cls = eval(obj.ClassInfo.SpaceName + '.TMyObject');
alert(cls.ClassName);
2. Qomo对象仍然是基于原型构造的
与其它的一些OOP框架不同的是:Qomo对象仍然是基于原型构造的。这表明开发人员仍然可以使用prototype来修改实例和构造器的属性。重要的是,这种修改对Qomo的类构造系统不会造成任何负面的影响。例如:
function MyObject() {
Attribute(this, 'Value', 200, 'r');
}
TMyObject = Class(TObject, 'MyObject');
MyObject.prototype.Value = 100;
var obj = new MyObject();
alert(obj.Value);
alert(obj.get('Value'));
Qomo 框架采用这种“对原型继承透明”的设计,使得它可以更容易地嵌入到其它的框或者系统中。大多数情况下,第三方的框架感觉不到Qomo的存在,也不会受到Qomo语法的任何影响。
3. Qomo对象构造的第二种语法
首先是作为一种习惯,我在Qomo中实现了一个“与Delphi完全一致”的语法:
function MyObject() {
}
TMyObject = Class(TObject, 'MyObject');
var obj = TMyObject.Create();
我们可以看到,其实“对象=类.Create()”在语义上更符合“类继承体系”。但是Qomo不希望将任何与JavaScript无关的语法或者语义“强加”给开发者。所以Qomo中仍然支持JavaScript中标准的构造器语法“对象=new 构造器()”。需要说明的是,这两种方法得到的对象实例没有任何的区别。
Qomo实现“类.Create()”语法的另一个原因,是因为在命名空间中,我们存储的是类的引用,而非构造器的引用。也就是说,上面的TMyObject能在命名空间中找到:
cls = <a_name_space>.TMyObject;
这种情况下,我们只能使用“类.Create()”这种语法。当然,这种情况下,仍然可以有两种选择:
cls = <a_name_space>.TMyObject;
// 方法一:
var obj1 = cls.Creae();
// 方法二:
var obj2 = new cls.Create();
// 错误的方法:
// var obj3 = new cls();
不要试图对一个类使用new()关键字,在Qomo中,"new cls()"这样的语义是不可理解的。