八、JavaScript面向对象的支持
~~~~~~~~~~~~~~~~~~
(续)
2. JavaScript面向对象的支持
--------
(续)
5). 使用instanceof关键字的运算
------
在JavaScript中提供了instanceof关键字来检测实例的类型。这在前面讨
论它的“五重身份”时已经讲过。但instanceof的问题是,它总是列举整个
原型链以检测类型(关于原型继承的原理在“构造与析构”小节讲述),如:
//---------------------------------------------------------
// instanceof使用中的问题
//---------------------------------------------------------
function MyObject() {
// ...
}
function MyObject2() {
// ...
}
MyObject2.prototype = new MyObject();
obj1 = new MyObject();
obj2 = new MyObject2();
document.writeln(obj1 instanceof MyObject, '<BR>');
document.writeln(obj2 instanceof MyObject, '<BR>');
我们看到,obj1与obj2都是MyObject的实例,但他们是不同的构造函数产生
的。——注意,这在面向对象理论中正确的:因为obj2是MyObject的子类实
例,因此它具有与obj1相同的特性。在应用中这是obj2的多态性的体现之一。
但是,即便如此,我们也必须面临这样的问题:如何知道obj2与obj1是否是
相同类型的实例呢?——也就是说,连构造器都相同?
instanceof关键字不提供这样的机制。一个提供实现这种检测的能力的,是
Object.constructor属性。——但请先记住,它的使用远比你想象的要难。
好的,问题先到这里。constructor属性已经涉及到“构造与析构”的问题,
这个我们后面再讲。“原型继承”、“构造与析构”是JavaScript的OOP中
的主要问题、核心问题,以及“致命问题”。
6). null与undefined
------
在JavaScript中,null与undefined曾一度使我迷惑。下面的文字,有利于
你更清晰的认知它(或者让你更迷惑):
- null是关键字;undefined是Global对象的一个属性。
- null是对象(空对象, 没有任何属性和方法);undefined是undefined类
型的值。试试下面的代码:
document.writeln(typeof null);
document.writeln(typeof undefined);
- 对象模型中,所有的对象都是Object或其子类的实例,但null对象例外:
document.writeln(null instanceof Object);
- null“等值(==)”于undefined,但不“全等值(===)”于undefined:
document.writeln(null == undefined);
document.writeln(null == undefined);
- 运算时null与undefined都可以被类型转换为false,但不等值于false:
document.writeln(!null, !undefined);
document.writeln(null==false);
document.writeln(undefined==false);
一、Qomolangma中的接口(Interface.js)
~~~~~~~~~~~~~~~~~~
在做AOP(面向切面编程)系统之前,我一直在想:有什么必要在JavaScript中做“接口(Interface)”的
机制。——当然,这也说明,你可能需要通过阅读迟些提供的、关于AOP框架的文档,才能理解如何使用
Qomo中强大的接口机制。^.^
接口是现代软件工程中的一种常用工具,它的出现使设计人员更多的关注于功能的“对外表现”,而非
“内部实现”。在软件模型设计中,类图通常用于描述设计的细部,而接口则更常用于描述模块、层次
间的交互关系。
然而这仍然停留在“设计”层面。在C++中,你会看到“接口是抽象类”这样的描述。换个说法,C++认
为接口只是“没有实现的类”。如果这样来描述的话,JavaScript中就没有必要实现一个“接口机制”。
所以,我们看到altas中,接口的实现就简单得多。例如:
---------------
// 1. 声明接口
Web.IArray = function() {
this.get_length = Function.abstractMethod;
this.getItem = Function.abstractMethod;
}
// 2. 注册接口,以及为类注册接口
Type.registerInterface("Web.IArray");
Type.registerClass('Web.UI.Data.DataControl', Web.UI.Control, Web.IArray);
// 3. 接口操作(方法)
Function.prototype.implementsInterface = function(interfaceType) { ... }
Function.prototype.isImplementedBy = function(instance) { ... }
---------------
同样的原因,altas中的接口也仅在极少数的地方得以应用。例如在属性检测(类似反射)时,有这样的
代码:
---------------
Web.TypeDescriptor.getProperty = function(instance, propertyName, key) {
if (Web.ICustomTypeDescriptor.isImplementedBy(instance)) {
return instance.getProperty(propertyName, key);
}
// ...
}
---------------
我们看到atlas中的接口主要用于“检测一个类(或对象实例)”是否实现过某接口。这用于保障后续代
码能安全的执行。本质上,altas是用registerInterface()的机制,来使开发人员不必使用'in'运算来
检测属性。因而上面的代码事实上写成这样也没有关系:
---------------
Web.TypeDescriptor.getProperty = function(instance, propertyName, key) {
if ('getProperty' in instance) {
return instance.getProperty(propertyName, key);
}
// ...
}
---------------
——然而,这仅仅只是“接口机制”应用的一个方面而已。
在Qomo中,接口不单单是描述,也是实现。实现的接口(的方法)可以被调用,这与Win32中的COM机制
是类同的。Qomo在实现了更加完整的接口特性,并在这样的基础之上,完整地实现了AOP的种种特性。
与JSEnhance.js一样,Qomo中的Interface.js可以脱离Qomo项目运行。
二、概念:接口是描述,也是实现
~~~~~~~~~~~~~~~~~~
接口最基础的定义就是“描述对象的行为”。所以根本上来讲,下面的声明:
---------------
Web.IArray = function() {
this.get_length = Function.abstractMethod;
this.getItem = Function.abstractMethod;
}
Type.registerClass('Web.UI.Data.DataControl', Web.IArray);
---------------
将表明下列的含义:
- 类Web.UI.Data.DataControl实现了Web.IArray接口
- 类Web.UI.Data.DataControl的实例(instance)将必然具有如下方法:get_length()和getItem()
接口描述了对象实例的行为能力,只是接口机制的一个方面。在COM框架、以及一些其它高级语言的接
口语义中,接口也表明了一种实现。这种“实现”体现在三个方面:
- 用户可以通过接口,来持有一组对象方法的引用
- 用户可以通过聚合、包含和委托等技术,来使对象包含多个接口
- 用户代码可以通过一个接口,来查询“实现接口的对象”的更多接口(以及行为能力)
例如(delphi source):
---------------
type
IMyObject = interface
// [a guid for COM framework]
function run(): boolean;
end;
TMyObject = Class(TObject, IMyObject)
//...
end;
var
obj : TMyObject;
intf : IMyObject;
// ...
obj := TMyObject.Create;
if obj.GetInterface(IMyObject, intf) then
intf.run();
---------------
在这个例子中,接口IMyObject可以用于变量的类型声明。而将intf声明为IMyObject类型,也表明
intf接口指针“从对象实例中取得有效的接口实现”。GetInterface()是COM中的QueryInterface()
的另一个实现,用于从obj中取接口引用。
一旦intf变量成功的获取一个“已实现的接口(一组对象方法指针的引用)”,那么接下来就可以调
用这些接口方法了,例如执行:intf.run()。
三、Qomo中的接口之一:接口的基本语法及实现
~~~~~~~~~~~~~~~~~~
1. 接口声明
----------
Qomo中的接口声明比较简单,采用与altas(以及其它OOP框架)兼容的代码:
---------------
IMyObject = function() {
this.method = Abstract;
}
IMyObject2 = function() {
this.method = Abstract;
this.method2 = Abstract;
}
---------------
Abstract是一个在Qomo中全局提供的、类似关键字的函数。这在讲述OOP框架时已经提及过。不过它
现在被从Object.js中移到了Interface.js中。Abstract()调用的结果是触发一个异常,这也意味着
使用new()创建出的任何一个接口实例,其方法调用都将失败。
Qomo中允许使用类似继承的方式来声明接口。但Qomo不推荐接口继承,因此如果你希望接口“继承
自”其它接口,那么建议用下面的代码来声明:
---------------
IMyObjectEx = function() {
IMyObject.call(this);
this.method_ex = Abstract;
}
---------------
使用显式的IMyObject.call()的原因,是使得开发人员在阅读代码时能清晰地了解这里有一个继
承关系。——换而言之,父级接口的修改将影响到子级接口的声明。
当然,这种方式也给一些接口声明带来了方便,例如在Interface.js中声明的:
---------------
IAspectedClass = function() {
IClass.call(this);
IJoPoints.call(this);
}
---------------
最后补充一点:不要在接口声明中对构造器加入口参数。因为接口不需要、也不应当通过可变参
数来“创建”。
2. 接口注册
----------
Qomo中接口注册有两种形式:
- Interface.RegisterInterface(obj, intf1 [, intf2 ... intfn]);
- Class(<BaseClass>, <ConstructorName> [, intf1 .. intfn]);
第一种形式可以为任何的JavaScript对象,包括原生的对象、函数等注册接口。这种形式存在一
个完全等义的全局函数RegisterInterface()。——事实上这指向同一个函数——在Qomo的内部代
码中,只使用Interface.RegisterInterface()这种完整形式。
第二种形式在Object.js中实现。Interface.js中用了少量的代码来使得“类的所有实例、和子类
拥有相同的接口”。如果你使用Qomo的OOP系统,那么只需要在调用Class()进行类注册的时候加
上一个实现的接口表即可。
可以通过Aggregate()来注册“聚合的”接口,这一部分放在后面单独讲述。
作为特例:对undefined和null对象注册接口返回一个-1值。
3. 接口查询
----------
Qomo中可用下面的形式来获取对象(含Qomo和原生的JavaScript对象)的注册接口:
- Interface.QueryInterface(obj, aInterface);
与注册过程相同,接口查询也有一个完全等义的全局函数QueryInterface(),并且Qomo内部只使
用完整形式的调用。
QueryInterface()将返回一个接口的实现,也就是一个可以调用的接口(对象/指针)。例:
---------------
function MyObject() { }
TMyObject = Class(TObject, 'MyObject');
var obj = new MyObject();
var intf = QueryInterface(obj, IObject);
document.writeln(intf.hasAttribute('Name'));
// output: false
---------------
如果查询null或undefined的接口,或者传入了无效的、未注册的接口同,QueryInterface()将
抛出异常。如果返回undefined,则表明不包含该接口。——此时在用户代码中,该接口引用应
该不被继续处理,而不是盲处理。
QueryInterface()能够查询通过Aggregate()来注册“聚合的”接口。
4. 其它
----------
接口系统提供一个函数来检测一个“声明”是否是接口。如果要在QueryInterface()之前检测一
个接口是否被注册过,可以使用如下函数(返回true/false):
- Interface.IsInterface(intf);
如果一段声明代码没有(隐式地)被注册到Qomo系统中,那么使用该声明来QueryInterface()会导致
一个异常。——将“接口声明”注册到Qomo的行为隐式地包含在RegisterInterface()和Class()等
调用的过程中。
如果你真的需要在一个接口被实现之前就注册它,那么你可以将它注册给IInterface。尽管这看起
来不怎么合理,但在Qomo中,这是唯一能被理解的方式:
---------------
IMyCustomIntf = function() { ... };
Interface.RegisterInterface(IInterface, IMyCustomIntf);
---------------
在Qomo内核中是具有“将声明直接注册(封装)成接口”的能力的,这就是Interface.js中实现过的
warpInterface()函数。但它不提供给用户代码使用。系统中通过这种方法注册的接口包括:
- IInterface
- IJoPoint
- IMuEvent
- IJoPoints
如果需要,你可以将自己需要(预先)注册的接口加入到Interface.js的代码中。当然,这并不是一
种推荐的形式。
Qomo提供一些对接口的演示和示例。在Qomo代码包中:
\Framework\DOCUMENTs\T_InterfaceQuery.html : 演示接口机制的基本原理
\Framework\DOCUMENTs\T_InterfaceAggregate.html : 演示脱离Qomo使用接口、聚合的基本使用
\Framework\DOCUMENTs\BaseObjectDemo4.html : Qomo中的接口系统的使用示例和基本特性
四、Qomo中的接口之二:(内部)聚合接口的语法
~~~~~~~~~~~~~~~~~~
在COM中,分离接口的方法包括“聚合”和“包含”;在Delphi中,在编译器级别提出了“委托”来
实现接口分离的技术。三者之间只是实现技术和时间上的差异,其效果是基本一致的。
基本上来说,接口隔离原则(Interface Segregation Principle)基于“使用多个专门的接口比使用
单一的总接口总要好”。因此,这事实上是使一个对象同时具有多个接口的技术。在Qomo中,下面两
种语法都可以实现这个目标:
- Interface.RegisterInterface(obj, intf1 [, intf2 ... intfn]);
- Class(<BaseClass>, <ConstructorName> [, intf1 .. intfn]);
然而,这两种接口注册的方式要求对象/类显示的具有接口指定的方法。也就是说:如果对象obj注册
了接口intf,则 intf.XXXXX 方法将直接指向obj.XXXXX方法。
然而,我们举个例子来说: TMyObject作为类,总是有对象构造过程的,这表明TMyObject应该具备这
样接口:
---------------
IObjectConstructor = function() {
this.OnNewInstance = Abstract;
}
---------------
但我们也知道,我们不能把一个OnNewInstnace事件放在TMyObject类上,或者放在obj实例上。——因
为它并不是实例的使用者所关注的。而且,更重要的是,“对象构造过程”在Qomo中被隐含在Class()
的实现代码内部,我们需要一种机制将它的这个能力public出来,这样才能做到“接口是实现”。
在Delphi中,接口的声明方法可以“在对象的私有域中实现”。这时可以通过接口“在对象/单元外”
访问该方法。这体现了“接口表现对象的能力,而不关注具体实现(是私有、公有或者委托)”这一接口
的基本原则。
在Qomo中,综合上述的这些问题及其解决方案,实现了“(内部的)聚合接口”这一特殊的机制。
1. 声明聚合(在函数内部)
----------
Qomo的聚合首先是“为了表现目标的内部具备的行为能力”而提供的。而JavaScript中,提供行为能
力的,只有对象(构造器)和函数。因此,“声明聚合”的行为通常应当发生在一个函数的内部。例如:
---------------
function MyFunc() {
// Aggregate()与RegisterInterface()有相同的入口参数
var intfs = Aggregate(MyFunc, intf1 [, intf2 .. intfn]);
}
---------------
由于这会导致每次执行MyFunc都会调用Aggregate(),因此Qomo推荐如下的语法来声明一个带有聚合接口
的函数:
---------------
MyFunc = function () {
function _MyFunc() {
// (函数自身的实现代码...)
}
var intfs = Aggregate(_MyFunc, intf1 [, intf2 .. intfn]);
// (实现接口 ...)
return _MyFunc;
}();
---------------
由于这样做仅是为了避免重复地调用Aggregate(),因此事实上用户也可以这样来声明(尽管同样麻烦):
---------------
function MyFunc () {
if (a_aggregated_tag == false) {
var intfs = Aggregate(_MyFunc, intf1 [, intf2 .. intfn]);
// (实现接口 ...)
a_aggregated_tag = true;
}
// (函数自身的实现代码...)
}
---------------
2. 实现接口
----------
Aggregate()只是建立对象与接口之间的关系并返回一个“Interfaces对象”,这个接口集合中所有的
接口都是未被实现过的。这仍然需要由用户代码来完成,例如:
---------------
IMyFunc = function() {
this.getLength = Abstract;
}
MyFunc = function () {
var arr = new Array(5);
function _MyFunc() {
// (函数自身的实现代码...)
}
// 0). 声明聚合
var intfs = Aggregate(_MyFunc, IMyFunc);
// 1). 取指定接口的一个引用
var intf = intfs.GetInterface(IMyFunc);
// 2). 实现接口方法
intf.getLength = function() {
return arr.length;
}
return _MyFunc;
}();
---------------
上面的代码中,“Interfaces对象”intfs的GetInterface()方法用于得到一个接口的引用,而接下来的
代码则描述该接口如何被实现。——在这里,可以使用“聚合”、“包含”或者“委托”这些技术之一。
(目前,)Qomo没有直接提供这三种技术的实现方案,只是“老老实实地”逐一实现了该接口的各个方法。
3. 使用接口
----------
前面已经提到过,Aggregate()内部聚合的接口的使用,与普通注册的接口是一样的。如上例:
---------------
var intf = Interface.QueryInterface(MyFunc, IMyFunc);
document.writeln('the length is: ', intf.getLength());
---------------
五、Qomo中的接口之三:接口的实现
~~~~~~~~~~~~~~~~~~
接口的使用示例请参见\Framework\DOCUMENTs\BaseObjectDemo4.html。内部聚合技术的使用示例可
参见\Framework\DOCUMENTs\T_InterfaceAggregate.html。下面讲述Qomo中实现这些技术的一些细节。
1. 基本接口功能的实现
----------
Qomo中的接口采用这样的结构来实现Interface关键字及相关的语法:
---------------
Interface = function() {
//...
function _Interface(obj) { /* Intf1 .. Intfn */
//...
}
_Interface.QueryInterface = function(obj, intf) { ... }
_Interface.RegisterInterface =_Interface;
_Interface.IsInterface = isInterface;
return _Interface;
}();
---------------
其中_Interface()用于实现RegisterInterface()与Interface(),它们是等义的。_Interface()首先
调用warpInterface()将除第一个参数之外的其它参数转变成接口,这其实是在该接口声明(函数)上附
加一个'_INTFHANDLE_'属性。——这是目前为止Qomo中唯一尝试改变对象对外的属性的地方。
由于接口本身只是“声明”而不能直接引用,因此加上该属性并不会导致什么麻烦。在warpInterface()
对该属性做了检测,以避免用户通过修改这个值来伪造、套取接口。——尽管这对用户来说也没有什么
意义。^.^
warpInterface()是真正实现RegisterInterface()的关键代码。_Interface()中的另一部分代码用于处
理与"聚合接口(Aggregate)"相关的一些逻辑。——这在后面进一步叙述。
接口中另一个重要的函数是_Interface.QueryInterface。它的实现相对要复杂些,因为它要处理:
- 对象的接口可以在类注册中通过Class()注册到指定的类,而不必为每一个对象实例注册;
- Qomo对象或者其它JavaScript原生对象都可以具有自己的接口注册;
- 函数可以注册内部的聚合接口。
QueryInterface()必要有能力从所有这些可能注册过接口的“对象”中查询到接口。而且还要返回该
接口的一个“有效的、能调用接口方法的”实现。
“实现接口”的基本原理可以参见\Framework\DOCUMENTs\T_InterfaceQuery.html。其基本步骤是:
- 创建指定接口的实例:intf = new IYour_Intf();
- 为接口的每一个“虚方法”重新封装一个调用方法,该方法将实现该接口的对象的同名方法;
- 特殊处理:任何对象都可以有IInterface接口,将持有QueryInterface()的一个引用。
2. 聚合接口功能的实现
----------
在对_Interface()的实现过程中特殊处理了Aggregate():
---------------
// register aggregated interfacds. is special!
if ((typeof obj == 'function') || (obj instanceof Function)) {
if (_Interface.caller===Aggregate && this!==Aggregate) {
// ...
}
}
---------------
也就是说如果:
- "obj"是一个函数对象,且
- 通过调用Aggregate()函数来注册它的接口,且
- this对象不是Aggregate()自身
则_Interface()将尝试把目标对象"obj"与一个"Interfaces"对象(this)分别添加到两个数组:
---------------
$Aggrs.push(obj); // 拥有接口的对象(总是一个函数)
$Aggri.push(this); // 所拥有的接口集合对象
---------------
而在Aggregate()工具函数中,将两次调用Interface.RegisterInterface(),也就是前面提到的
_Interface()函数。这两次调用的作用并不一样:
第一次采用标准格式调用RegisterInterface(),这时传入的this将是_Aggregate()函数的引用:
---------------
Interface.RegisterInterface.apply(_Aggregate, arguments);
---------------
代码中,由于Aggregate()与RegisterInterface()的入口参数声明是一样的,因此apply()的第
二个参数直接使用了arguments。这样就首先将接口注册到了指定的对象上。
第二次采用特殊格式调用RegisterInterface(),这时传入的this将是“接口集合”Interfaces
的一个实例,而唯一的一个入口参数foo,将指向被注册接口的函数对象引用:
---------------
return Interface.RegisterInterface.call(new Interfaces(arguments), foo);
---------------
这次调用,RegisterInterface()将返回传入的this,也就是“接口集合”对象。这样做只是为了
省掉在_Aggregate()中一个临时变量。
上面的代码流程表明,一个“(内部的)聚合接口”至少具有拥有如下信息:
- 一个Interfaces对象。用于存放所有实现过的接口列表;
- 在_Interface()中的Interfaces与foo的对照表“$Aggrs与$Aggri”中存放一对信息。用于实
现getAggrInterfaces()函数,并用于支持QueryInterface()对该函数foo的查询;
- 在实现接口的函数内部,持有Interfaces对象的一个引用。它用于各接口的实现。
五、其它
~~~~~~~~~~~~~~~~~~
1. 缺省注册的接口
----------
Qomo为的一些全局对象、函数和基类注册了缺省的接口。这些接口定义在Interface.js中。缺
省注册的对象包括:
注册到 接口 说明
--------------------------------------------------------------------
* IInterface 包括undefined、null在内的所有JS原生对象和Qomo对象等
$import() IImport (beta版暂未实现)
MuEvent() IMuEvent 所有多投事件句柄: 由MuEvent()构造的实例(beta版暂未实现)
JoPoint() IJoPoint 所有的联接点: 由JoPoint()构造的实例
TObject() IObject Qomo OOP系统的基类
TClass IClass 所有通过Class()注册的类
Class() IClassRegister 类注册函数
Class()等 IJoPoints Qomo内部为一些能提供AOP能力的函数注册了IJoPoints接口
Aspect() IAspect Qomo AOP系统的切面基类
2. 如何为特定的对象(如JavaScript原生对象)注册接口
----------
在Qomo的(目前的)发布版本中,对一些JavaScript原生对象未提供接口支持。例如对Array()对象。
这只是因为Qomo没有觉察到这样做的必要,因而没有加入相关的代码。这在技术实现上其实非常简
单。下面参考对MuEvent()的实现,介绍一下Array()的接口实现方法:
---------------
// 以下代码填写在interface.js中
// 1. 接口声明
IArray = function() {
this.push = Abstract;
this.pop = Abstract;
// ...
}
// 2. Interface()的实现代码中,warpInterface()函数声明之后加入代码
warpInterface(IArray);
// 3. 在isImplemented()的实现代码中,修改(添加)如下代码
switch (intf) {
// ...
case IArray: return this instanceof Array;
}
---------------
这样就完成了。
一、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()的参数说明如下:
- 1. base : 必须是this
- 2. name : 指定特性名. 习惯上以大写开头.
- 3. value: 初值
- 4. 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('<methodName>' [, 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()还有两种可能的用法:
----------
// 语法三: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()"这样的语义是不可理解的。
posted on 2006-08-13 00:48
汪杰 阅读(347)
评论(0) 编辑 收藏 引用 所属分类:
javascript