第六章 面向对象程序设计(3.继承)

  ·   JS高设(第三版)   ·   JavaScript     浏览量:

ES实现继承主要依靠原型链

一、原型链

ES中描述了原型链的概念,并将原型链作为继承的主要方法。
其基本思想是利用原型让一个引用类型继承里一个引用类型的属性和方法。
简单回顾一下构造函数、原型和原型实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型的内部指针。
那么假如我们让原型对象等于另一个类型的实例,结果会怎样呢?显然,此时原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着指向另一个构造函数的指针。假如另一个圆形又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。

1
上图中,定义了Supertype和SubType两个类型。每个类型分别有一个属性和方法。他们的主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋值给SubType.prototype实现的。
实现的本质是重写原型对象,代之以一个新类型的实例。
原来存在于SuperType中的所有属性和方法现在都存在于SubType中,在确认继承关系之后还为SubType.protoType添加了一个新的方法。
他们的内部关系如下图所示
1
在上面的代码中,我们没有使用SubType默认提供的原型,而是给他换了一个新的原型;这个新的原型就是SuperType的实例。
于是新原型不仅具有作为一个SuperType的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了SuperType的原型。结果就是这样的:instance指向SubType的原型,SubType又指向Super Type的原型。getSuperValue()仍然在SuprerType.prototype中,但property仍然则位于SubType中。因为property是一个实例的属性,而getSuperValue()是一个圆形方法。既然现在SubType.prototype是SuperType的一个实例,那么property当然位于该实例中了。此外,要注意instance.constructor现在指向的是SuperType,这是因为原来SubType.prototype中的constructor被重写的缘故。
实际上,不是SuperType的原型的constructor属性被重写了,而是SubType的原型指向了另一个对象SuperType的原型,而这个圆形对象的constructor属性指向的是SuperType
通过实现原型链,本质上扩展了本章前面介绍的圆形搜索机制当访问一个属性时,首先搜索实例属性,在搜索实例原型。在通过原型链实现继承的情况下,搜索过程就得沿着原型链继续向上。就那上面的例子来说,调用instace.setSuperValue()会经历三个搜索步骤:
1).搜索实例;
2)搜索SubType.prototype;
3)搜索SuperType.prototype,

最后一步才会找到该方法。在找不到属性或者方法的情况下,搜索过长总要一环一环的前行到原型链末端才会停下来。

  • 别忘记默认的原型

事实上,前面的例子中展示的原型链还少一环。我们知道,所有引用类型默认几层了Object,而这个继承也是通过原型链实现的。大家要记住,所有的函数默认圆形都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。所以我们说上面的例子展示的原型链中还应该包括另外一个继承层次。
2
总之一句话,Sub继承了Super,Super继承了Object。调用instance.toString()的时候,实际上是调用的Object.prototype中的哪个方法。

  • 确定原型和实例的关系

可以通过两种方式来确定原型和实例之间的关系
1)instanceof()
2)isPrototypeOf()

console.log(instance instanceof Object);    //true
console.log(instance instanceof SuperType); //true
console.log(instance instanceof SubType);   //true

console.log(Object.prototype.isPrototypeOf(instance));     //true
console.log(SuperType.prototype.isPrototypeOf(instance));  //true
console.log(SubType.prototype.isPrototypeOf(instance));    //true
  • 谨慎的定义方法

子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎么样,给原型添加方法的代码一定要放在替换原型语句之后。来看下面的例子
5
上面代码是正确的用法,SubType重写了getSupeerValue()方法,但是通过SuperType的实例调用的还是原来的方法,还有一点需要注意,不能使用字面量床架圆形方法,这样会重写原型链

SubType.prototype = {
    getSubValue : function(){...}
    
    someOtherMethod : function() {...}
}

以上代码将原型替换成了一个字面量对象,由于现在的圆形包含的是一个Object的实例,而非SuperType的实例,因此我们设想中的原型链已经被切断--SuperType和SubType已经恩断义绝。

  • 原型链问题

原型链虽然强大可以实现继承。但是引用类型的原型属性会被所有实力共享,而这也就是为什么要在构造函数中,而不是圆形中定义属性。再通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是原先的实例属性也就顺理成章的变成了现在的原型属性了,请看以下代码
6
例子中SuperType构造函数定义了一个属性colors,包含一个数组(引用类型值)。SuperTyoe的每一个实例都会有半酣自己数组的colors属性。当SubType通过原型链继承了SuperType之后,SubType.prototype变成了SuperType的一个实例,因此他也拥有了他自己的colors属性--就跟专门创建了一个SubType.prototype.colors一样。但结果时SU偶有SubType的实例都共享这一个colors属性。因此修改instance1会在instance2中反映出来,如上图
原型链的第二个问题是:在创建子类时,不能向超类型的构造函数中传递参数。
实际上应该说是,没有办法再不影响所有对象实例的情况下,给超类传递参数。
所以很少在实践中单独使用原型链

二、借用构造函数

基本思想相当简单,就是在子类构造函数中调用超类构造函数
用来解决原型中包含引用类型值(如数组)带来的问题,如下
7
上图中,实现继承那一行,通过call()方法(或者apply),我们实际上是在未来将要新创建SubType实例的环境下调用了SUperType构造函数。这样会在新SubType对象上执行SuperType()中定义的所有对象初始化代码。每个实例中都有自己的colors副本了。

  1. 传递参数

可以在子类构造函数中向超类构造函数传递参数
8
2) 问题
如果方法都在构造函数中定义,因此复用函数就无从谈起了。而且在超类中定义的方法,对子类而言是不可见的,结果所有类型都只能使用构造函数模式,因此很少单独使用

三、组合继承

最常用的继承模式
将原型链和借用构造函数结合,原型链实现对原型属性和方法的继承,构造函数实现实例属性继承
1

上图:SuperType有name,colors俩个构造函数属性,一个sayName()原型方法。
SubType构造函数在调用SuPerType的时候,传入了name参数,定义了自己的属性age。
然后将,SuperType的实例赋值给SubType的原型,然后又在新原型定义sayAge()方法。
这样两个不同的SubType实例即拥有了自己的属性,又使用了想通的方法。

四、原型式继承

这种方法并没有严格意义上的构造函数,而是借助原型可以基于已有的对象创建函数,同时还不必因此创建自定义类型,为了达到这个目的,给出了如下函数

function(o){
    functioni F(){}
    F.prototype = o;
    return new F();
}

object()函数内,先创建一个临时构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时对象的新实例。
从本质上讲,object()对传入其中的对象进行了一次浅复制
1

这种原型式继承要求必须有一个对象作为另一个对象的基础,把他传递给object(),然后再根据具体需求对得到的对象加以修改即可。
上图例子中,person()就是基础对象,传入object()中返回一个新的对象。这个新对象将person作为原型,所以他的原型中包含一个基础类型值属性和一个引用类型值属性。
这意味着person.friends不仅属于person,也会被anotherPerson以及yetAnotherPerson共享。实际上就相当于又创建了person对象的两个副本。
ES5通过新增Object.create()方法规范了原型式继承。这个方法接受两个参数:一个是作为新对象原型的对象和(可选)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与object()方法的行为相同。
2

Object.crearte()的第二个参数指定的任何属性都会覆盖原型上同名属性
3

在没有必要兴师动众创建构造函数,只想让一个对象与另一个对象保持雷瑟的情况下,原型式继承完全胜任,但是别忘了,引用类型值的属性始终对会共享值(如数组),就像使用原型模式一样。

五、寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真正的是他做了所有工作一样返回对象。
看不懂了,以后再写



收起 >>
第六章 面向对象程序设计(3.继承)