对象的继承方案
1.副本扩充
JavaScript的对象属于一种继承方案(Object Hierarchy)。对象的属性可以拥有另一个子对象,创建父子关系的对象方案。例如:nameCard 对象拥有一个子对象 PhoneList,这个子对象保存家庭电话和手机电话号码,如下所示:
function nameCard(name,age,phone,email)
{
this.name = name;
this.age = age;
this.phone = new phoneList(phone,"N/A");
this.email = email;
}
上述构造函数 nameCard() 和 phone 属性创建另一个副本,使用phoneList() 构造函数,如下所示:
function phoneList(homephone,cellphone)
{
this.homephone = homephone;
this.cellphone = cellphone;
}
上述构造函数 phoneList() 拥有两个参数 homephone 和 cellphone。在nameCard()中,参数只传递一个Phone参数,被设置为homephone属性,cellphone属性默认为N/A。如果需要设置手机电话号码请使用下列的程序代码,如下所示:
objMyCard.phone.cellPhone = "0901-6666666";
上述的程序代码objMyCard对象为nameCard()构造函数创建的对象副本。在phone对象的cellphone属性设置手机电话号码,很明显的可以看出cellphone属于phone子对象的属性。
如果某一个人拥有两只手机,虽然,我们可以重新修改构造函数,添加一个属性,不过,Internet Explorer的5.0版以后支持expando属性。对象属性能够在Javascript程序代码动态的添加,我们称为副本扩充(Instance Extension)。这不同于Prototype对象类别的扩充(Class Extension)
换句话说,只需替objCard对象添加一个属性就可以保存额外的手机的电话号码,如下所示:
objCard.cellphone = "0900-7777777";
上述程序代码的objCard对象为nameCard()创建的对象副本。我们替这个对象副本添加cellphone属性,这个属性是专属于objCard对象副本的,另一个对象副本objMyCard中并没有这个属性。
此时objCard对象副本将可以访问两个cellphone属性,一个是使用expando属性所添加的cellphone属性,另一个是访问子对象phone的objCard.phone.cellphone属性。
事例:
<script type="text/javascript">
// 对象的构造函数
function nameCard(name,age,phone,email)
{
this.name = name;
this.age = age;
this.phone = new phoneList (phone,"N/A");
this.email = email;
}
// 对象的构造函数
function phoneList(homephone,cellphone)
{
this.homephone = homephone;
this.cellphone = cellphone;
}
// 创建自定义对象
var objMyCard = new nameCard("小猪一",33,"02-2222222222","zhuyi@live.com");
// 设置手机电话号码
objMyCard.phone.cellphone = "0901-6666666";
var objCard = new nameCard("小猪二",25,"02-3333333333","zhuer@live.com");
// 添加另一只手机的属性
objCard.cellphone = "0900-7777777";
// 显示 objMyCard 对象属性
document.write("姓名:" + objMyCard.name + "<br>");
document.write("电话:" + objMyCard.phone.homephone + "<br>");
document.write("手机1:" + objMyCard.phone.cellphone + "<br>");
document.write("手机2:" + objMyCard.cellphone + "<br>");
// 显示objCard对象属性
document.write("姓名:" + objCard.name + "<br>");
document.write("电话:" + objCard. phone.homephone + "<br>");
document.write("手机1:" + objCard. phone.cellphone + "<br>");
document.write("手机2:" + objCard. cellphone + "<br>");
</script>
2.对象冒充
构想原始的ECMAScript时,根本没打算设计对象冒充(object masquerading)。它是在开发者开始理解函数的工作方式,尤其是如何在函数环境中使用this关键字后才发展出来的。
其原理如下:构造函数使用this关键字给所有属性和方法赋值(即采用类声明的构造函数方式)。因为构造函数只是一个函数,所以可使ClassA的构造函数成为ClassB的方法,然后调用它。ClassB就会收到ClassA的构造函数中定义的属性和方法。例如,用下面的方式定义ClassA和ClassB:
function ClassA(sColor)
{
this.color = sColor;
this.sayColor = function()
{
alert(this.color);
}
}
function ClassB(sColor){}
还记得吗?关键字this引用的是构造函数当前创建的对象。不过在这个方法中,this指向的是所属的对象。这个原理是把ClassA作为常规函数来建立继承机制,而不是作为构造函数。如下使用构造函数ClassB可以实现继承机制:
function ClassB(sColor)
{
this.newMethod = ClassA;
this.newMethod(sColor);
delete this.newMethod;
}
在这段代码中,为ClassA赋予了方法newMethod(记住,函数名只是指向它的指针)。然后调用该方法,传递给它的是ClassB构造函数的参数sColor。最后一行代码删除了对ClassA的引用,这样以后就不能再调用它。
所有的新属性和新方法都必须在删除了新方法的代码行后定义。否则,可能会覆盖超类的相关属性和方法:
function ClassB(sColor,sName)
{
this.newMethod = ClassA;
this.newMethod(sColor);
delete this.newMethod;
this.name = sName;
this.sayName = function()
{
alert(this.name);
}
}
为证明前面的代码有效,可以运行下面的例子:
var objA = new ClassA("red");
var objB = new ClassB("blue","Nicholas");
objA.sayColor(); // outputs "red"
objB.sayColor(); // outputs "bule"
objB.sayName(); // outputs "Nicholas"
有趣的是,对象冒充可以支持多重继承。也就是说,一个类可以继承多个超类。用UML表示的多继承机制如图所示。
例如,如果存在两个类ClassX和ClassY,ClassZ想继承这两个类,可以使用下面的代码:
function ClassZ()
{
this.newMethod = ClassX;
this.newMethod();
delete this.newMethod;
this.newMethod = ClassY;
this.newMethod();
delete this.newMethod;
}
这里存在一个弊端,如果ClassX和ClassY具有同名的属性或方法,ClassY具有高优先级,因为继承的是最后的类。除这点小问题之外,用对象冒充实现多继承机制轻而易举。
由于这种继承方法的流行,ECMAScript的第三版为function对象加入了两个新方法,即call()和apply()。
3.call() 方法
call()方法是与经典的对象冒充方法最相似的方法。它的第一个参数用作this的对象。其他参数都直接传递给函数自身。例如:
function sayColor(sPrefix,sSuffix)
{
alert(sPrefix + this.color + sSuffix);
}
var obj = new Object();
obj.color = "red";
// outputs "The color is red,a very nice color indeed."
sayColor.call(obj,"The color is",",a very nice color indeed.");
在这个例子中,函数sayColor()在对象外定义,即使它不属于任何对象,也可以引用关键字this。对象obj的color属性等于"red"。调用call()方法时,第一个参数是obj,说明应该赋予sayColor()函数中的this关键字值是obj。第二个和第三个参数是字符串。它们与sayColor()函数中的参数prefix和suffix匹配,最后生成的消息"The color is red, a very nice color indeed"将被显示出来。
要与继承机制的对象冒充方法一起使用该方法,只需将前三行的赋值、调用和删除代码替换即可:
function ClassB(sColor,sName)
{
//this.newMethod = ClassX;
//this.newMethod();
//delete this.newMethod;
ClassA.call(this,sColor);
this.name = sName;
this.sayName = function()
{
alert(this.name);
}
}
这里,想让ClassA中的关键字this等于新创建的ClassB对象,因此this是第一个参数。第二个参数sColor对两个类来说都是唯一的参数。
4.apply:
使用apply(function.apply)方法
构造函数:
public apply(thisObject:Object, [argArray:Array]) : Void
指定要在 ActionScript/Javascript 调用的任何函数内使用的 thisObject 的值。此方法还指定要传递给任何被调用函数的参数。由于 apply() 是 function 类的方法,所以它也是 ActionScript 中每个 function 对象的方法。
与 function.call() (它将参数指定为用逗号分隔的列表)不同,该方法将参数指定为一个 Array 对象。如果在脚本实际执行前,无法知道要传递的参数的数量,那么这种方法通常很有用。
参数
thisObject:Object - 要应用 myfunction 的对象。
argArray:Array [可选] - 一个数组,其元素作为参数传递给 myfunction。
例如:
function sayColor(sPrefix,sSuffix)
{
alert(sPrefix + this.color + sSuffix);
}
var obj = new Object();
obj.color = "red";
// outputs "The color is red,a very nice color indeed."
sayColor.apply(obj,"The color is",",a very nice color indeed.");
这个例子与前面的例子相同,只是现在调用的是apply()方法。调用apply()方法时,第一个参数仍是obj,说明应该赋予sayColor()中的this关键字值是obj。第二个参数是由两个字符串构成的数组,与sayColor()的参数prefix和suffix匹配。生成的消息仍是"The color is red, a very nice color indeed",将被显示出来。
该方法也用于替换前三行的赋值、调用和删除新方法的代码:
function ClassB(sColor,sName)
{
//this.newMethod = ClassX;
//this.newMethod();
//delete this.newMethod;
ClassA.apply(this,new Array(sColor));
this.name = sName;
this.sayName = function()
{
alert(this.name);
}
}
同样的,第一个参数仍是this。第二个参数是只有一个值color的数组。可以把ClassB的整个arguments对象作为第二个参数传递给apply()方法:
function ClassB(sColor,sName)
{
//this.newMethod = ClassX;
//this.newMethod();
//delete this.newMethod;
ClassA.apply(this,arguments);
this.name = sName;
this.sayName = function()
{
alert(this.name);
}
}
当然,只有超类中的参数顺序与子类中的参数顺序完全一致时才可以传递参数对象。如果不是,就必须创建一个单独的数组,按照正确的顺序放置参数。此外,还可使用call()方法。
5. 原型链
继承这种形式在ECMAScript中原本是用于原型链的。上一章介绍了定义类的原型方式。原型链扩展了这种方式,以一种有趣的方式实现继承机制。
在上一章中学过,prototype对象是个模板,要实例化的对象都以这个模板为基础。总而言之,prototype对象的任何属性和方法都被传递给那个类的所有实例。原型链利用这种功能来实现继承机制。
如果用原型方式重定义前面例子中的类,它们将变为下列形式:
function ClassA()
{
ClassA.prototype.color = "red";
ClassA.prototype.sayColor = function()
{
alert(this.color);
}
}
function ClassB(){}
ClassB.prototype = new ClassA();
原型链的神奇之处在于突出显示的代码行。这里,把ClassB的prototype属性设置成ClassA的实例。这很有意义,因为想要ClassA的所有属性和方法,但又不想逐个将它们赋予ClassB的prototype属性。还有比把ClassA的实例赋予prototype属性更好的方法吗?
注意,调用ClassA的构造函数时,没有给它传递参数。这在原型链中是标准做法。要确保构造函数没有任何参数。
与对象冒充相似,子类的所有属性和方法都必须出现在prototype属性被赋值后,因为在它之前赋值的所有方法都会被删除。为什么?因为prototype属性被替换成了新对象,添加了新方法的原始对象将被销毁。所以,为ClassB类添加name属性和sayName()方法的代码如下:
function ClassB()
{
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function()
{
alert(this.name);
}
}
提示:上面的例子最后的结果 ClassB的对象是没有 ClassA 的方法和属性的。这是因为ClassA 的 prototype 写在构造函数里面的时候,在创建一个ClassB 对象时运行到 ClassB.prototype = new ClassA() 会结束创建ClassB的对象,但是原型在创建完此构造函数之后仍然会被创建,不过在此时ClassB的对象时是不会存在ClassA的属性方法的,而在下一次再创建ClassB对象的时候就会有ClassA里面的属性方法了。
比如:
function ClassB()
{
ClassB.prototype.test = "test" ;
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function()
{
alert(this.name);
}
}
var objB = new ClassB();
这样子的结果是 objB.test 存在 而这次的创建并没有继承ClassA的属性方法。
但是只要继续创建一个ClassB的对象的时候就会有了。
var objB = new ClassB();
objB.color="bule";
objB.sayColor(); //outputs "blue";
所以建议在对 prototype 属性进行赋值的时候,最好不要写在构造函数的里面。
比如:
function ClassB()
{
}
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function()
{
alert(this.name);
}
这样子是比较不错的办法,因为在执行的时候,程序会先执行prototype 的赋值,然后再创建ClassB的对象的。这样子就避免了上面的一些问题。
可通过运行下面的例子测试这段代码:
var objA = new ClassA();
var objB = new ClassB();
objA.color = "red";
objB.color = "blue";
objB.name = "Nicholas";
objA.sayColor(); // outpus "red"
objB.sayColor(); // outputs "blue"
objB.sayName(); // outputs "Nicholas"
需要注意的地方:在创建原型的时候
此外,在原型链中,instanceof运算符的运行方式也很独特。对ClassB的所有实例,instanceof为ClassA和ClassB都返回true。例如:
var objB = new ClasB();
alert(objB instanceof ClassA); // outputs "true"
alert(objA instanceof ClassB); // outputs "true"
在ECMAScript的弱类型世界中,这是极其有用的工具,不过使用对象冒充时不能使用它。
原型链的弊端是不支持多重继承。记住,原型链会用另一类型的对象重写类的prototype属性。
6.混合方式
这种继承方式使用构造函数定义类,并未使用任何原型。对象冒充的主要问题是必须使用构造函数方式,这不是最好的选择。不过如果使用原型链,就无法使用带参构造函数了。开发者该如何选择呢?答案很简单,两者都用。
在前面,学过创建类的最好方式是用构造函数方式定义属性,用原型方式定义方法。这种方式同样适用于继承机制,用对象冒充继承构造函数的属性,用原型链继承prototype对象的方法。用这两种方式重写前面的例子,代码如下:
function ClassA(sColor)
{
this.color = sColor;
}
ClassA.prototype.sayColor = function()
{
alert(this.color);
}
function ClassB(sColor,sName)
{
ClassA.call(this,sColor);
this.name = sName;
}
ClassB.prototype= new ClassA();
ClassB.prototype.sayName = function()
{
alert(this.name);
}
在此例子中,继承机制由两行突出显示的代码实现。在第一行突出显示的代码中,在ClassB构造函数中,用对象冒充继承ClassA类的sColor属性。在第二行突出显示的代码中,用原型链继承ClassA类的方法。由于这种混合方式使用了原型链,所以instanceof运算符仍能正确运行。
下面的例子测试了这段代码:
var objA = new ClassA("red");
var objB = new ClassB("blue","Nicholas");
objA.sayColor(); // outputs "red"
objB.sayColor(); // outputs "blue";
objB.sayName(); // outputs "Nicholas"