深入探索Javascript对象机制
时间:2006-09-19 来源:softwindow
问题(1)由于javascript是采取prototype机制的“伪继承”,prototype必须显示引用“基类”对象,所以注定了javascript只能实现“弱继承”,或者叫做“对象继承”
注意这里的次序关系需要很明确,prototype的赋值必须在对象构造之外。
例如:
function classA()
{
classA.prototype.methodA = function() {...}
}
function classB()
{
classB.prototype.methodA = function(){...}
}
classB.prototype = new classA();
//注意这里声明B继承自A出现在function classB()函数体之外,并且先于
//classB对象的构造被执行。
由于是这样一种机制,所以有两点需要注意:
第一,在类的构造函数中不应当执行任何持久化操作,如全局的或者dom对象的构造,因为如果你这么做了,当使用这个类作继承的时候,即使你只实例化一个对象,这个类的构造函数也可能被执行超过一次!(相反地,在真正面向对象的语言,如C++和Java中,当我们采用Singleton模式的时候,常常被允许在构造函数中执行持久化操作,如果习惯了这种模式,在javascript中很容易出错!)
例如:
function uniqueForm(url)
{
this.id = 'myform';
this.action = url
uniqueForm.prototype.Form_Submit = function(){...}
this.form = document.creatElement('form');
//问题出在这里,在构造函数中进行了持久化操作
......
}
function uniqueValidateForm(url)
{
......
......
}
uniqueValidateForm.prototype = new uniqueForm();
var myForm = new uniqueValidateForm('test.aspx');
//这个语句被执行的时候其实已经构造的是第二个form元素了,
//第一个元素在继承语句uniqueValidateForm.prototype = new uniqueForm();
//中已经被构造出来了。
一个避免这个问题的方法是采用工厂:
function formFactory()
{
formFactory.getCurrentForm = function()
{
if(formFactory.form == null)
{
formFactory.form = document.createElement('form');
}
return formFactory;
}
}
把上面例子中两个类的document.createElement('form');改成formFactory.getCurrentForm()即可。
问题(2)类属性的继承:问题的根源同样在于javascript的对象继承机制。通过prototype实现的继承很容易获得基类的对象属性和对象方法,但是却根本不可能获得基类的类方法。而真正的面向对象继承,应当是同时继承类和对象方法才行。一种基本上可以解决这个问题的方案是使用反射遍历基类的方法加到子类方法中去。例如:
function classA()
{
classA.prototype.methodA = function() {...} //这是一个对象方法
classA.methodA = function(){...} //这是一个类方法
}
function classB()
{
......
}
classB.prototype = new classA();
for (each in classA)
{
classB[each] = function(){classA[each]}; //将classA的类方法也继承下来
}
var obj = new classB();
obj.methodA(); //调用继承自A的对象方法
classB.methodA(); //调用继承自A的类方法
问题(3)带参数的基类构造:正如上面提到的,由于prototype和对象继承的限制,在构造派生类对象之前必须先实例化基类对象,这给我们实现带参继承带来了很大的麻烦。一种解决方法是等到需要构造对象的时候才去处理继承
例如:
classB.prototype = new classA(1, 2); //在一次构造之前才实现继承
var objB = new classB(1, 2);
classB.prototype = new classA(-1, -2); //另一次构造,参数不同所以要重写继承
var objC = new classB(-1, -2);
当然,像上面这样的做法给人的感觉很不好,都有点不像是继承了。
还有另外一种做法是令B的构造函数不依赖于A的构造函数,也就是说在B中重写所有A中实现的构造函数逻辑,而仅仅继承B的属性域和方法域,这样在声明继承的时候只要提供给A的构造函数任意一组合法的参数就行了。当然这种方法也很麻烦。
事实上要解决上面这个问题,必须考虑在B中保留对A域的访问并且将构造函数逻辑抽象出来。
例如:
function classA(x, y)
{
classA.prototype.constructor = function(x, y)
{
......
}
......
if (x != null || y!=null) this.constructor(x, y);
}
function classB(x, y)
{
classB.prototype.constructor = function(x, y)
{
this.base.constructor(x, y); //先调用基类的构造函数
...... //再执行自己的构造函数
}
......
if (x != null || y!=null) this.constructor(x, y);
}
classB.prototype = new classA();
classB.prototype.base = new classA();//用无参继承
var objB = new classB(1, 2);
var objC = new classB(-1, -2);
2)事件驱动机制和回调函数实现
javascript的灵活性使得其实现事件驱动和回调是很方便的,唯一的困扰就在于this指针。一般来说,在面向对象中我们希望对象方法的this指针仅仅指代对象本身,而不应当有其他含义。在一般情况下,this指针在javascript的对象方法中也能很好的工作,但是很可惜在面对回调和事件驱动的时候就不适用了。根本原因是因为javascript的this指针是随着调用者而改变的。在一般的对象方法中,调用者自然是对象本身,然而在回调函数和事件函数中,调用者可能是外部对象或者事件的真正传播者。
例如:
function classA()
{
this.button = document.createElement('input');
this.button.type = 'button';
......
......
this.button.onclick = classA.prototype.Button_Click;
this.text = '请点击';
this.count = 0;
......
......
......
classA.prototype.Button_Click = function() //这样写得不到正确的结果
{
alert('点击次数:" + ++this.count); //因为此时的this已经代表调用者
this.button.text = '请再次点击'; //button,而不是classA对象了
}
}
在复杂的事件和回调中,可能还会包含成员函数之间的相互调用和事件的关联,我们当然不能将大量的精力花费在判断this指针所属上,一种比较巧妙的解决方法是引入一种固定的事件注册模式,如上面的代码写成:
function classA()
{
this.button = document.createElement('input');
this.button.type = 'button';
......
......
this.button.eventHandler = this;
this.button.onclick = function(){this.eventHandler.Button_Click(this, event);};
//注意这个函数内的this实际上在执行的时候代表button
this.text = '请点击';
this.count = 0;
......
......
......
classA.prototype.Button_Click = function(sender, event) {
alert('点击次数:" + ++this.count); //正确了
this.button.text = '请再次点击'; //或者sender.text更为简单
}
}
3)抽象类(abstract Classes)
抽象类是面向对象设计中的一种重要元素,要发挥javascript面向对象的多态力量,必须要寻找一种实现抽象类的机制。
Javascript的灵活性使得我们可以用一种简单的方式实现它
例如:
function abstractA(Implemented)
{
abscractA.prototype.A = function(){...}
......
......
......
if (Implemented != true)
{
throw new Error('抽象类abstractA不能构造实例!');
}
}
function classA()
{
......
}classA.prototype = new abstractA(true);
4)接口:接口也是一种非常重要的元素,然而实现它比较复杂
下面是一个例子:
function InterfaceA(objA)
{
if (objA.methodA == objA.methodA)
throw new Error('InterfaceA的方法MethodA在对象中未实现!");
if (objA.methodA == objA.methodB)
throw new Error('InterfaceA的方法MethodB在对象中未实现!");
......
......
}
function classA()
{
......
}classA.prototype = new InterfaceA(new classA());
5)高级反射技术——元数据管理和多态回调
我们利用javascript类生成了一个对象之后,如何能够在外部快速地获得这个对象的句柄,反射技术为我们提供了一个思路,甚至我们还能在运行时环境中动态地获得一个类家族的所有实例而不需要事先知道它们的对象名。另外,这种技术的一个重要用途是实现局部对象的持久化。
由于这是一个比较复杂的应用,只有在一些复杂的任务中才能体会到它的便利,这里只能举一个极其简单的例子:
function classA(id)
{
this.id = id; //这个ID对于元数据的建立是重要的
classA.instances[id] = this;
classA.instances.All.push(this);
......
......
}
classA.instances = new Array();
classA.instances.All = new Array();
var objA = new classA('myObjA');
在外部用classA.instances['myObjA']可以直接访问objA而无需知道对象名objA,另外还可以通过对classA.instances.All遍历来访问所有classA的实例。