原型链与继承

1.原型链

1.概念

ECMAScript只支持实现继承,是依靠原型链来实现的。

每个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个指向原型对象的内部指针。当试图引用对象(实例instance)的某个属性,会首先在对象内部寻找该属性,如果找不到,会在对象的原型(instance.__proto__)里去找这个属性。

如果让原型对象指向另一个类型的实例,那么有趣的事情就发生了。

即:constructor1.prototype = instance2。其中instance2constructor2的一个实例。

此时,我们生成一个实例 instance1 = new constructor1();。然后尝试在instance1中查找某个属性p

1.首先,程序会在instance1内部属性查找一遍。

2.接着会在instance1.__proto__(constructor1.prototype)中查找一遍,而constructor1.prototype实际是instance2,也就是会在instance2的内部属性中查找p

3.如果instance2也没有内部属性p,程序会继续在instance2.__proto__(constructor2.prototype)中寻找,如果还没有找到,会一直向上寻找,直至Object的原型对象。

搜索轨迹:instance1-->instance2-->constructor2.prototype...-->Object.prototype

这种搜索的轨迹,形似一条长链,又因为prototype在查找中充当链接的作用,于是把这种实例与原型的链条叫做原型链

可以通过instanceOf操作符和isPrototypeOf()方法可以判断实例和原型的关系。

instanceOf操作符,只要是原型链中出现过的构造函数,结果都会返回true。

isPrototypeOf()只要是原型链中出现过的原型,结果都会返回true。

2.原型链的问题

然而,原型链并不完美,它有以下两个问题:

问题一:当原型链中包含引用类型值的原型时,该引用类型值会被所有的实例共享。

问题二:在创建子类型时,不能向超类型的构造函数中传递参数。

为此,实践中很少单独使用原型链,通常会有采取其他方法来弥补原型链的不足。

2.js继承的方式与实现

下面介绍几种继承方式,它们的关系是:

1.借用构造函数

为了解决原型链中的问题,我们开始使用一种叫做借用构造函数的技术。

基本思想:即在子类型构造函数的内部调用超类型构造函数。

从上面的代码可以看出:

第一,原型链中引用类型的值是独立的,不再被所有实例共享。

第二,子类型在创建时也可以向父类型传递参数。

随之而来的,如果仅仅借用构造函数,那么将无法避免构造函数模式存在的问题-方法都在构造函数中定义,因此函数的复用也就不可用了。

2.组合继承

组合继承,也叫伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式。

基本思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。

这样既能通过在原型上定义方法实现复用,又能保证每个实例都有它自己的属性。

组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,称为JavaScript中最常用的继承模式。而且,instanceOfisPrototypeOf()也能用于识别基于组合继承创建的对象。

但是,这个方式也存在一点问题,它调用了两次父类构造函数,造成了不必要的消耗。

3.原型继承

这个方法最初由道格拉斯·克罗克福德提出,他说道借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

方法:在object()函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。

从本质上讲,object()对传入其中的对象执行了一次浅复制。下面的例子说明了问题:

在上面的代码中,person对象作为基础对象,生成了两个新的对象,这两个对象将person作为原型,因此它的原型上就包含引用类型值属性。这就意味着peroson.friends不仅归person所有,其也会被anotherPersonyetAnotherPerson共享,他们也可以读写这个对象属性。

在ECMAScript5中,通过新增的Object.create()方法规范了上面的原型式继承。

Object.create()接收两个参数:

  • 一个用作新对象原型的对象

  • 一个可选的为新对象定义额外属性的对象

上面的代码可以看出,Object.create()只有一个参数时功能与之前的object()方法相同,它的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的,以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

目前支持Object.create()的浏览器有IE9+,Firefox 4+, Safari 5+, Opera 12+ 和 Chrome。

原型继承中,包含引用类型值的属性始终都会共享响应的值,就像使用原型模式一样。

4.寄生式继承

寄生式继承与原型式继承紧密相关的一种思路,同样是道爷推而广之。

基本思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点和构造函数模式类似。

5.寄生组合式继承

最完美、最常用的继承方式就要呼之欲出啦~

前面讲过,组合继承是JavaScript最常用的继承模式,但是由于它有自己的不足-无论什么情况下,都会两次调用父类构造函数:一次是在创建子类原型的时候,一次是在子类构造函数内部。那么,寄生组合式继承,完美地解决了这个问题,降低了调用父类构造函数的开销。

基本思想:不必为了指定子类型的原型而调用超类型的构造函数。

extend的高效率体现在它没有调用SuperClass构造函数,因此避免了在subClass.prototype上面创建不必要多余的属性,与此同时,原型链还能保持不变。因此能正常使用instanceOf和isPrototypeOf()方法。

以上,寄生组合式继承,集寄生式继承和组合继承的优点于一身,是实现基于类型继承最有效的方法。

下面,我们来看extend的另一种更有效的扩展。

为什么要执行new F(),既然extend的目的是将子类型的prototype指向超类型的prototype,为什么不直接赋值呢?

显然,上面的操作之后,子类型原型与超类型的原型共用,根本没有继承关系。

6.ES6 class继承

ES6出现了class的语法,并给出了基于class的继承方法,这个方法和寄生组合式继承的原理基本相同。实现如下:

ES5的继承,实质上是先创造了子类的实例对象this,然后再将父类的方法添加到this上面(Father.call(this))。ES6的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(调用super方法),然后再用子类的构造函数修改this

3.new运算符

new运算符具体干了什么工作?它主要做了以下三件事:

1.创建一个空对象obj

2.将这个空对象的__ptoto__属性指向了函数Fprototype属性。

3.将F函数对象的this指针替换成了obj

其实,new操作符调用构造函数时,函数内部实际发生了以下变化:

4.重载

重载:就是函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。

JavaScript本身是不支持重载的,因为js的函数时无态的,它并不要求传入的参数的类型和个数要和函数定义时严格相等。

那么,实现js的重载可以根据参数的类型不同和参数个数不同两个方向来考虑。

方法一:根据参数个数实现重载

我们实现一个加函数,这个函数会把所有输入的参数相加后返回。

在上面的代码中,我们使用arguments来判断参数个数,然后进行相加返回。实现了add()函数的重载。在jquery中,attr()函数就是根据参数个数来实现多态的。传入两个参数时表示取值,三个参数时表示赋值。

方法二:根据参数类型实现重载

我们实现一个根据参数类型来相加的add()函数,这个参数接收两个参数。如果传入的参数都是数字,则按照数字的规则相加,如果传入的参数是字符串,则拼接字符串。

在上面的代码中,根据传入参数的类型不同采取不同的行为实现重载。

5.多态

多态:是同一行为具有多个不同表现形式或形态的能力。多态就是同一个接口,使用不同的实例而执行不同的操作。

多态和继承紧密相关,我们来实现一个多态的例子。

首先,我们定义一个动物类。

然后分别定义猫和狗两个类,重写它们的run()方法。

测试一下,我们实现的效果:

上面,我们实现了同一接口,不同实例执行不同的操作,也就是多态。

6.参考文献

JavaScript高级程序设计(第三版)

详解JS原型链与继承arrow-up-right

js重载和多态arrow-up-right

Javascript 的继承与多态arrow-up-right

Last updated