例21.19 实例继承法.html
- <html>
- <head>
- <title>例21.19 实例继承法</title>
- </head>
- <body>
- <script>
- <!--
- function dwn(s){
- document.write(s + "<br/>");
- }
- function MyDate(){
- var instance = new Date(); //instance是一个新创建的日期对象
- instance.printDate = function(){
- document.write("<p>" + instance.toLocaleString() +"</p>");
- } //对instance扩展printDate()方法
- return instance; //将instance作为构造函数的返回值返回
- }
- var myDate = new MyDate();
- dwn(myDate.toGMTString());
- myDate.printDate();
- -->
- </script>
- </body>
- </html>
输出结果:
Sat, 27 Oct 2012 05:48:24 GMT
Sat Oct 27 2012 13:48:24 GMT+0800 (中国标准时间)
通常情况下要对JavaScript原生的核心对象或者DOM对象进行继承时,我们会采用这种继承方法。
缺点:首先,由于它需要在执行构造函数的时候构造基类的对象,而JavaScript的new运算与函数调用不同的是不能用apply()方法传递给它不确定的arguments集合,这样就会对那些可以接受不同类型和不同数量参数的类型的继承造成比较大的麻烦。
其次,这种继承方式是通过在类型中构造对象并返回的办法来实现继承的,那样的话new运算的结果实际上是类型中构造的对象而不是类型本身创建的对象,对象的构造函数将会是实际构造的对象的构造函数而不是类型本身的构造函数,尽管你可以通过赋值的办法修正它,但是你却无法修正instanceof表达式的结果。
第三,这种方法一次只能返回一个对象,它和原型继承法一样不能支持多重继承。
实例继承法也不是一种真正的继承法,它也是一种模拟。构造继承法是目前所知的唯一一种可以较好地继承JavaScript核心对象的继承法。
21.3.2.4 拷贝继承法及其例子
拷贝继承法就是通过对象属性的拷贝来实现继承。
- <html>
- <head>
- <title>例21.20拷贝继承法</title>
- </head>
- <body>
- <script>
- <!--
- Function.prototype.extends = function(obj){
- for(var each in obj){
- this.prototype[each] = obj[each];
- //对对象的属性进行一对一的复制,但是它又慢又容易引起问题
- //所以这种“继承”方式一般不推荐使用
- }
- }
- //定义一个Point类型
- function Point(dimension){//有无参数没有关系,方正拷贝继承法只是把一个对象的属性和方法拷贝过去而已
- this.dimension = dimension;
- }
- function Point2D(){}
- Point2D.extends(new Point(2));
- var point2D = new Point2D();
- alert(point2D.dimension);
- -->
- </script>
- </body>
- </html>
拷贝继承法实际上是通过反射机制拷贝基类对象的所有可枚举属性和方法来模拟“继承”,因为可以拷贝任意数量的对象,因此它可以模拟多继承,又因为反射可以枚举对象的静态属性和方法,所以它同构造继承法相比的优点是可以继承父类的静态方法。但是由于是反射机制,因此拷贝继承法不能继承非枚举类方法,例如父类中重载的toString()方法,另外,拷贝继承法也有几个明显的缺点,首先是通过反射机制来复制对象属性效率上非常低下。其次它也要构造对象,通常也不能很好地支持灵活的可变参数。第三,如果父类的静态属性中包含引用类型,它和原型继承法一样导致副作用。第四,当前类型如果有静态属性,这些属性可能会被父类的动态属性所覆盖。最后这种可支持多重继承的方式并不能清晰地描述出父类与子类的相关性。
21.3.2.5 几种继承法的比较
表21.1 比较几种继承方法的优劣
比较项 |
构造继承 |
原型继承 |
实例继承 |
拷贝继承 |
静态属性继承 |
N |
Y |
Y |
Y |
内置对象继承 |
N |
部分 |
Y |
Y |
多参多重继承 |
Y |
N |
N |
Y |
执行效率 |
Y |
Y |
Y |
N |
多继承 |
Y |
N |
N |
Y |
Instanceof |
false |
true |
flase |
false |
21.3.2.6混合继承法及其例子
混合继承是将两种或者两种以上的继承同时使用,其中最常见的是构造继承和原型继承混合使用,这样能够解决构造函数多参多重继承的问题。
例21.21混合继承法.html
- <html>
- <head>
- <title>例21.21混合继承法</title>
- </head>
- <body>
- <script>
- <!--
- function Point2D(x,y){
- this.x = x;
- this.y = y;
- }
- function ColorPoint2D(x,y,c){
- Point2D.call(this,x,y);
- //这里是构造继承,调用了父类的构造函数
- this.color = c;
- }
- ColorPoint2D.prototype = new Point2D(3,4);
- //这里用了原型继承,让ColorPoint2D以Point2D对象为原型
- var colorPoint2D = new ColorPoint2D(1,2,"red");
- document.write("x:" + colorPoint2D.x + "<br/>");
- delete colorPoint2D.x;
- document.write("删除x之后:" + colorPoint2D.x);
- -->
- </script>
- </body>
- </html>
运行结果:
x:1
删除x之后:3
另外,在模拟多继承的时候,原型继承和部分条件下的拷贝继承的同时使用也较常见。
21.3.3单继承与多重继承
在面向对象中,继承一般分为单继承和多重继承两种模式。其中,单继承模式比较简单,它要求每个类型有且仅有一个父亲。而多重继承是一种比较复杂的模式,它允许一个类型拥有任意多个父亲。 并不是所有的面向对象语言都支持多重继承,Java和C#就不从语法上直接支持它(Java和C#中,多重继承都是通过接口来模拟的)。
在现实生活中,一些事物往往会拥有两个或者两个以上事物的特性,用面向对象思想来描述这些事物,其中的一种常用模式就是多重继承。
JavaScript的原型继承机制不支持多重继承,用其他模拟继承方法,特别是拷贝继承,是可以实现JavaScript的多重继承的。
例21.22多重继承.html
- <html>
- <head>
- <title>例21.21混合继承法</title>
- </head>
- <body>
- <script>
- <!--
- Function.prototype.extends = function(obj){
- for(var each in obj){
- this.prototype[each] = obj[each];
- }
- }
- function Transportation(){
- this.transportationName = "Transportation";
- }
- function Motorcar(){
- this.motorcarName = "Motorcar";
- }
- //Motorcar继承Transportation
- Motorcar.extends(new Transportation());
- function Ship(){
- this.shipName = "Motorcar";
- }
- //Ship继承Transportation
- Ship.extends(new Transportation());
- function TwoRoots(){
- this.twoRootsName = "Motorcar";
- }
- TwoRoots.extends(new Motorcar());
- TwoRoots.extends(new Ship()); //TwoRoots同时继承Motorcar和Ship
- -->
- </script>
- </body>
- </html>
面向对象中并不是所有的事物泛性都只能用继承这样的关系来描述,继承关系只是泛化关系的一种类型,除此以外,创建关系、原型关系以及聚合关系和组合关系,都是泛化关系中的类型。“泛型”的概念是很广义的。通常我们用继承、聚合和组合来描述事物的名词特性,而用原型、元类等其他概念来描述事物的形容词特性。
21.3.4接口及其实现
接口是一种纯抽象的定义。接口是指那些并没有具体实现,只是定义出“原型”的类型。在JavaScript中,prototype既有“原型”,也具有接口的特征。
- <script>
- IPoint = function(){this.x=undefined,this.y=undefined};
- var Point = function(){};
- Point.prototype = new IPoint();
- var p = new Point();
- for(var each in p){
- document.write(each + "<br/>"); //包含有属性x和y,因为Point实现了IPoint接口
- }
- </script>
运行结果:
x
y
更广义地说,接口是一种抽象概念,实现或者匹配一个接口,并不依赖于特定的语言语法,
21.3.5多态及其实现
在面向对象中,一个实例可以拥有多个类型,在实际程序计算中,它既可以被当成是这种类型,又可以被当成是那种类型。这样的特性,我们称之为“多态”。
例21.25实现多态.html
- <html>
- <head>
- <title>例21.25实现多态</title>
- </head>
- <body>
- <script>
- <!--
- function dwn(s){
- document.write(s + "<br/>");
- }
- //定义一个Animal类型
- function Animal(){
- this.bite = function(){
- dwn("animal bite!");
- }
- }
- //定义一个Cat类型,继承Animal类型
- function Cat(){
- this.bite = function(){
- dwn("cat bite!");
- }
- }
- Cat.prototype = new Animal();
- //定义一个Dog类型,继承Animal类型
- function Dog(){
- this.bite = function(){
- dwn("dog bite!");
- }
- }
- Dog.prototype = new Animal();
- //定义一个AnimalBite多态方法
- function AnimalBite(animal){
- if(animal instanceof Animal){
- animal.bite(); //Cat bite or dog bite
- }
- }
- //构造一个Cat对象
- var cat = new Cat();
- //构造一个Dog对象
- var dog = new Dog();
- //Cat和Dog都是Animal,AnimalBite是一个多态函数
- AnimalBite(cat);
- AnimalBite(dog);
- -->
- </script>
- </body>
- </html>
运行结果:
cat bite!
dog bite!
JavaScript是天生多态的弱类型语言:
例21.26 最简单的“多态”函数.html
- <script>
- function add(x,y){
- return x + y;
- }
- document.write(add(10,20) + "<br/>");
- document.write(add("a","b"));
- </script>
运行结果:
30
ab
构造与析构指的是创建与销魂对象的过程,它们都是对象生命周期中最重要的环节之一。
2.4.1构造函数
构造函数是面向对象的一个特征,它是在对象被构造时运行的函数。在C++等静态语言中,对象的结构是在声明时被固化的,构造函数的作用只是进行某些必要的初始化工作。而在JavaScript中,new操作符作用的函数对象就是类型的构造函数。由于JavaScript的动态特性,理论上讲,JavaScript的构造函数可以做任何事情,轻易地改变对象的结构。
JavaScript中,对象的contructor属性总是引用对象的构造函数。
例21.27(1)构造函数.html
- <script>
- function Point(x,y){
- this.x = x || 0;
- this.y = y || 0;
- }
- var p = new Point();
- document.write(p.constructor);
- </script>
运行结果:
function Point(x, y) { this.x = x || 0; this.y = y || 0; }
在构造函数拥有引用类型返回值的时候(实例继承法),真正的对象是被返回的那个对象,从这一点来看,contructor属性似乎违反了前面的原则,然而事实上,由于对象的返回值代替了new操作符返回的对象本身,因此实际的contructor值返回的是被构造的对象。如果你要对其进行修改,让contructor看起来像是“类型”本身,直接对contructor属性进行赋值即可。
例21.27(2)改写contructor.html
- <script>
- function ArrayList(){
- var ret = new Array();
- ret.constructor = this.constructor;
- return ret;
- }
- document.write((new ArrayList()).constructor);
- </script>
运行结果:
function ArrayList() { var ret = new Array; ret.constructor = this.constructor; return ret; }
21.4.2多重构造
多重构造是指在面向对象的继承中,子类构造函数和父类构造函数的依赖关系。一般来说,在面向对象的语言中,子类被构造时,总是先依次执行祖先类的构造函数,再执行子类构造函数本身,不过JavaScript本身并没有这样的文法特性。
例21.28多重构造.html
- <script>
- //定义一个Point类型
- function Point(dimension){
- this.dimension = dimension || 0;
- this.isRegular = function(){
- this.dimension > 0;
- }
- }
- //定义一个Point2D类型,继承Point类型
- function Point2D(x,y){
- Point.call(this,2);
- var ponds = [];
- ponds.push(x,y);
- this.x = {
- valueOf:function(){ return ponds[0] },
- toString:function(){ return ponds[0] }
- };
- this.y = {
- valueOf:function(){ return ponds[1] },
- toString:function(){ return ponds[1] }
- };
- }
- //构造ColorPoint2D时将执行Point2D.call(),这导致Point2D的构造,而Point2D构造时
- //再执行Point的构造,这种从对象自身的构造开始依次执行父类构造函数的过程
- //就叫做“多重构造”
- function ColorPoint2D(x,y,c){
- Point2D.call(this,x,y);
- this.color = c;
- }
- var colorPoint2D = new ColorPoint2D(1,2,'red');
- document.write("color:" + colorPoint2D.color + "<br/>");
- document.write("x:" + colorPoint2D.x + "<br/>");
- document.write("y:" + colorPoint2D.dimension + "<br/>");
- </script>
运行结果:
color:red
x:1
y:2
有不少人习惯于在声明对象时将构造函数抽象出来,这不仅可以更加灵活地控制对象的构造,还可以在原型继承时比较有力地支持构造函数的“多态”。
例21.29抽象出构造函数.html
- <script>
- //定义Point2D类型
- function Point2D(x,y){
- //_init是Point2D类型的构造函数
- function _init(x,y){
- this.x = x;
- this.y = y;
- }
- if(x != null && y != null){
- _init.call(this,x,y);
- }
- }
- //这个例子里将构造函数抽象成了_init()方法,这样更加灵活便于控制
- //定义ColorPoint2D类型,继承Point2D类型
- function ColorPoint2D(x,y,c){
- //_init是ColorPoint2D类型的构造函数
- function _init(x,y,c){
- Point2D.call(this,x,y);
- this.color = c;
- }
- if(x != null && y != null && c != null){
- _init.call(this,x,y,c);
- }
- }
- var colorPoint2D = new ColorPoint2D(1,2,'red');
- document.write("colorPoint2D:" + colorPoint2D.color + "<br/>");
- document.write("colorPoint2D:" + colorPoint2D.x + "<br/>");
- </script>
运行结果:
colorPoint2D:red
colorPoint2D:1
21.4.2析构
在面向对象的概念中,析构是指销魂对象时执行的动作,默认的析构是由语言环境本身提供的,而某些语言如C++允许用户自己订制的析构过程,这个过程被作为对象的一个特殊的方法,称为“析构函数”。
例21.30析构.html
- <script>
- var Disposable = {
- dispose : function(){
- //遍历并回收对象的每一个属性,注意这里递归检查dispose()
- for(var each in this){
- if(this[each] instanceof Disposable){
- this[each].dispose();
- }
- this[each] = null;
- }
- }
- }
- function Point(){}
- //通过原型“继承”的方式给Point类型的对象dispose()方法
- Point.prototype = Disposable;
- </script>
在很多情况下,对象的析构能够用较小的代价充分地释放资源,大大提高JavaScript的空间效率,比较有效地避免内存泄漏,然而要注意的是,析构函数本身的执行会带来额外的时间开销,因此在做出选择时要仔细地权衡利弊。不过析构函数在许多对空间要求相对严格的应用中会显得很有用。