因为这三种实现继承的方式都是有关联的,所以将他们放在一起了(其实可以将原型链继承也放进来)。
一、借用构造函数实现继承
基本思想:在一个构造函数里面通过call或者apply调用另一个构造函数,从而实现将这个构造函数里的属性和方法继承过来。
看下例子:
function constructorParent(wealth = ['house', 'car']) {
this.wealth = wealth
}
function constructorSon(args) {
constructorParent.call(this, args)
}
const p1 = new constructorSon()
console.log(p1.wealth) // ["house", "car"]
const p2 = new constructorSon(['money'])
console.log(p2.wealth) // ["money"]
这种方法实现继承,有优点也有缺点:
- 优点
- 1、可以传参,这一点显而易见,看例子就有。
- 2、每个实例都会单独生成一个副本,不像原型链继承那样,共享一个原型。所以,对于继承过来的属性,想怎么改就这么改,并不会影响到其他实例。
- 缺点
- 1、只要是用到构造函数,不管是用来创建对象还是用来实现继承,都会带来一个固有的弊端:无法实现函数复用。在上面优点中已经提到,每个实例都会单独生成一个副本,所以每个实例也就会重新生成一个方法,而在js中,方法也是一个对象,也就是实例化了一个对象,这样会造成一些不必要的开销。
- 2、只能继承构造函数方法体内的属性和方法,而无法继承构造函数原型上的属性和方法。这一点上面的例子没有提到,可以看一下:
function constructorParent(wealth = ['house', 'car']) {
this.wealth = wealth
}
constructorParent.prototype.sayName = function() {
console.log('constructorParent')
}
function constructorSon(args) {
constructorParent.call(this, args)
}
const p1 = new constructorSon()
p1.sayName()
需要注意的点
如果想要给自身添加属性和方法的时候,需要在调用call或者apply之后添加。因为如果继承过来的属性和自身属性存在同名的话,后面的会覆盖前面的,为保证不被覆盖,可以在继承之后添加。这一点例子也没有体现,可以看下:
function constructorParent(wealth = ['house', 'car']) {
this.wealth = wealth
this.friends = ['Tony']
}
function constructorSon(args) {
this.wealth = ['$']
constructorParent.call(this, args)
this.friends = ['Tina']
}
const p1 = new constructorSon()
console.log(p1.wealth, p1.friends)
很明显,wealth就被覆盖了。
二、组合继承
组合:组合构造函数和原型链
基本思想:通过原型链实现对原型属性和方法的基础,通过构造函数实现对实例属性的基础。通俗一点的讲,就是方法和一些需要被所有实例共享的属性通过原型链来继承,而一些不需要共享的属性就通过构造函数继承。
看例子:
function constructorParent(wealth = ['house', 'car']) {
// wealth 不需要被所有实例共享,每个实例的值都不尽相同,所以放到构造函数里
this.wealth = wealth
}
// 方法放原型对象上
constructorParent.prototype.sayName = function(name) {
console.log(name)
}
// sex 每个实例都是一样的,放到原型对象上
constructorParent.prototype.sex = 'male'
function constructorSon(args) {
// 构造函数继承,实现对实例属性的基础
constructorParent.call(this, args)
}
// 原型链继承,实现对原型属性和方法的基础
constructorSon.prototype = new constructorParent()
const p1 = new constructorSon(['house'])
const p2 = new constructorSon(['car'])
console.log(p1.wealth, p2.wealth)
console.log(p1.sex, p2.sex)
p1.sayName('p1')
p1.sayName('p2')
集合了原型链继承和构造函数继承的优点同时还避免了他们的缺点,可以说是一种很完美的继承模式了。
三、寄生组合式继承
既然说组合模式是一种很完美的继承模式了,还要这个寄生组合式继承干嘛?它更好吗?
没错,它还真就更完美了!
组合继承虽然说融合了原型链继承和构造函数继承的有点同时避免了他们各自的缺点,但是在融合两者的过程中,不可避免的又产生了新的问题,即需要被继承的构造函数会被调用两次,构造函数继承的时候调用一次,原型链继承的时候调用一次。调用两次构造函数带来的结果是什么呢?首先,调用两次函数必然会降低效率,其次,我们来打印一下上面例子中的p1:
可以看到,会有两组wealth,一组在实例上,一组在原型上。那么,这些问题,就需要靠寄生组合式继承来解决了。
基本思想:改下组合继承,组合继承中的借用构造函数继承不动,那么就得改一下原型链继承部分了:增加一个额外的函数,该函数负责创建一个需要继承的原型的副本,然后将这个副本替换继承者的原型。实际上这里用的是寄生式继承的思路(可以看下js继承-原型式继承、寄生式继承)。这也就是为什么叫寄生组合式继承了。
看下具体例子:
function constructorParent(wealth = ['house', 'car']) {
// wealth 不需要被所有实例共享,每个实例的值都不尽相同,所以放到构造函数里
this.wealth = wealth
}
// 方法放原型对象上
constructorParent.prototype.sayName = function(name) {
console.log(name)
}
// sex 每个实例都是一样的,放到原型对象上
constructorParent.prototype.sex = 'male'
function constructorSon(args) {
// 构造函数继承,实现对实例属性的基础
constructorParent.call(this, args)
}
extendPrototype(constructorParent, constructorSon)
// 额外的函数,将需要继承的原型做成副本并赋给继承者的原型上
function extendPrototype(constructorParent, constructorSon) {
// 创建一个需要继承的原型的副本
const prototype = Object(constructorParent.prototype)
// 将prototype的constructor指向constructorSon
prototype.constructor = constructorSon
// 用prototype替换constructorSon的prototype
constructorSon.prototype = prototype
}
const p1 = new constructorSon()
p1.sayName('p1')
console.log(p1.sex)
这样,extendPrototype函数就代替了原型链继承的那部分,也就不用再执行一次构造函数了。打印一下p1:
可以看到,wealth不会出现在原型上了。
THE END