原型链继承
每个构造函数都有一个原型对象,其中包含的是指向构造函数的指针,而构造函数的实例都包含了指向原型对象的内部指针。原型链继承的实现方式就是让父类的原型对象等于超类构造函数的实例,通过继承的方式,使子类能够拥通过原型链得到属性和方法。
1 | function SuperType(){ |
子类的原型指向父类,而父类的原型指向超类的原型,通过原型链搜索机制的了解,当访问子类属性时,首先在子类上找,然后到父类的原型,最后到超类的原型上查找,最后一步是Object。
确定原型与实例关系
- instance
测试的是实例与原型链中出现过的任何构造函数的关系。以上的例子中,instance实例可以是父类、超类及Object任何一个的实例,所以测试结果都是true
。
1 | console.log(instance instanceof Object);//true |
- isPropertyOf
只要是原型链中出现过的原型也都会返回true
1 | console.log(Object.prototype.isPrototypeOf(instance));//true |
注意点
父类定义方法时,不能使用对象字面量的方式进行定义,即使
constructor
指针设置为超类。
通过这样的方式定义,会切断原型链,子类就无法继承到超类的方法。
1 | SubType.prototype = new SuperType(); |
原型链的缺点
如果超类中的属性是引用类型,那么所有的子类都能共享到这一属性,一旦某个子类修改了这一属性,那么其他子类都会受到影响。
创建子类时,无法向超类构造函数中传递参数。
鉴于以上的两点,很少单独使用原型链继承。
借用构造函数继承
使用
call
或apply
在(将来)新创建的对象上执行构造函数,具体的实现方式是:在子类的构造函数内部调用超类的构造函数。这个继承模式可以解决无法向超类构造函数传递参数的问题。
但是这种模式也许根本称不上继承,因为已经和原型链
没有一点关系了,都是通过构造函数获得父类和超类的属性,因此也无法通过继承得到超类原型上定义的方法。
还有一点,因为借用了构造函数,每一次实例化后的对象中的方法都是不同的,所以无法实现代码复用。
1 |
|
组合继承
利用原型链实现原型属性和方法的继承,借用构造函数实现对实例属性的继承,在原型上定义的方法能够复用,也可以保证每个实例都有自己的属性。
1 |
|
通过在父类上调用构造函数,子类可以得到超类的属性,从构造函数继承可以知道,即使这些属性是引用类型属性,也可以互不干涉。而在之后的原型链继承,可以将超类型的方法继承给子类使用。
原型式继承
这种方法没有用到严格意义上的构造函数,借助原型可以基于已有的对象创建新对象。函数将传入的对象进行一次浅复制之后返回,每一次调用这个函数都相当于创建了传入对象的副本。
1 | function object(o){ |
对于object
函数的这个功能,与ES5定义的Object.create()
在只传入一个参数的情况下,它们得到的结果是相同的。
而Object.create
方法的第二个参数与Object.defineProperties
方法的第二个参数格式相同,每个属性都通过自己的描述符定义,且会覆盖原型对象上的同名属性。
1 | var person = { |
因为从本质上来说是浅复制,所以所有的属性都会共享到相应的值。这一点和原型模式相同。
寄生组合式继承
使用组合继承并不是万无一失的选择,这种继承方式调用两次超类构造函数,一次是原型链继承调用,一次是构造函数调用。而父类型的原型上也会获得超类型的属性,这不一定是必要的。
为了减少一次调用,避免出现以上的问题,可以使用寄生式继承来继承超类型的原型,然后再将结果指定给父类型的原型。
实现的基本模式如下:
1 | function inheritPrototype(Subtype,SuperType){ |
inheritPrototype
函数只是将超类型原型的副本继承给了父类型,而不是通过赋值直接重写的。所以不会得到不必要的属性。与此同时也能保持原型链的作用。所以这种模式是最理想的选择。