目录
一、原型链继承
1. 基本思想
原型链继承的基本思想是通过原型来继承多个引用类型的属性和方法
实现的基本思路是利用构造函数实例化对象,通过 new
关键字,将构造函数的实例对象作为子类函数的原型对象。
2. 实现方法
// 定义父类函数
function Father() {
// 定义父类属性
this.name = 'father'
}
// 给父类的原型添加方法
Father.prototype.say = function () {
console.log('我是爸爸');
}
// 创建子类函数
function Son() {}
// 实现继承
Son.prototype = new Father()
// 打印参考
console.log(Son.prototype) // Father {name: "father"}
解释:首先定义了一个父函数和子函数,添加了一些属性和方法
而实现继承的关键在于
Son.prototype = new Father()
。那它怎么理解呢首先我们需要了解一下
new
操作符的执行过程
- 创建一个空对象
- 继承函数原型,将这个新对象的
__proto__
属性赋值为构造函数的原型对象- 构造函数内部的
this
指向新对象- 执行函数体
- 返回这个新对象
new
的过程后,我们可以知道当我们在 new Father()
操作时,这一步将 Father
构造函数的原型对象打包给了 Father
的实例对象,也就是 father.__proto__ = Father.prototype
,换到这里也就是 Son.prototype.__proto__ = Father.prototype
,这样一来也就是将父类的实例对象作为了子类的原型,这也一来就在子类与父类实现了连接
关键性代码:
son.prototype = new Father()
3. 存在的问题
在这个例子中:
function Father() {
// 定义父类属性为引用数据类型
this.a = [1, 2, 3, 4]
}
我们将上面的代码中 a
的值改成引用数据类型,我们知道对于引用数据类型只会保存对它的引用,也就是内存地址。
我们先创建两个继承这个父类的子类 son1 ,son2
let son1 = new Son()
let son2 = new Son()
接着我们想向 son1
中的 a
数组添加一个值 5 ,我们会这么操作
son1.a.push(5)
打印一下此时的son2 中
a
数组也被改变了,而这就是原型链继承方式带来的引用数据类型被子类共享的问题
4. 优点与不足
优点:
- 父类的方法可以复用
- 操作简单
缺点
- 对于引用数据类型数据会被子类共享,也就是改一个其他都会改
- 创建子类实例时,无法向父类构造函数传参,不够灵活。
二、借用构造函数继承
为了解决原型链继承方式带来的引用值无法共享的问题,从而兴起了一种“盗用构造函数继承”的方式
1. 基本思想
为了想要实现引用值共享的问题,我们就不能给子类直接使用原型对象上的引用值。
因此,可以在子类构造函数中调用父类构造函数。
function Son() {
this.a = [1, 2, 3, 4]
}
如果我们将子类的代码改写成这样,当我们通过 Son
构造函数实例化实例对象时,每个实例对象中变量 a
都是独立的,属于自身的,当我们修改一个时,不会影响另一个的值
2. 实现方法
function Father() {
this.a = [1, 2, 3, 4]
}
function Son() {
Father.call(this)
}
let son1 = new Son()
let son2 = new Son()
son1.a.push(5)
console.log(son1, son2)
我们可以看到,在上面的实现方式中,并没有直接采用 this.a...
而是采用了 Father.call(this)。
我们原先直接将 this.a
直接的写在了子类函数里面,这和直接在子类中调用 Father
方法是类似的,唯一的差别就是 this
指向问题。
如果直接的在子类中调用
Father()
,那么它的this
将指向window
,这样就无法将数据绑定到实例身上,因此我们需要改变this
的指向,指向当前的子类构造函数这样一来就能将数据绑定到了每个实例对象身上。
同时由于我们的关键语句采用的是 call
,因此我们可以给父类构造函数传递参数,实现传递参数
3. 存在的问题
如果在父类构造函数的原型上添加方法,则会出现以下问题:
Father.prototype.say = function () {
console.log(111);
}
即:无法在子类上找到 say
方法
4. 优点与不足
优点:
- 解决了无法共享引用值的问题
- 能够传递参数
缺点:
- 只能继承父类的实例属性和方法,不能继承父类的原型属性和方法
- 父类方法无法复用。每次实例化子类,都要执行父类函数。重新声明父类所定义的方法,无法复用。
三、组合式继承
在前面两种方法中,都存在着一定的缺陷,所以很少会将它们单独使用。为此一种新的继承方式就诞生了:组合继承(伪经典继承),组合继承结合了原型链与盗用构造函数继承的方式,将两者的优点结合在一起。
1. 基本思想
通过原型链继承方式继承父类原型上的属性和方法,再使用盗用构造函数的方式继承实例上的属性
这样,实现了把方法定义在原型上以实现复用,又保证了让每个实例都有自己的属性
2. 实现方法
function Father() {
this.a = [1, 2, 3, 4]
}
Father.prototype.say = function () {
console.log(111);
}
function Son() {
Father.call(this)
}
Son.prototype = new Father()
let son1 = new Son()
let son2 = new Son()
其实只是在盗用构造函数的基础上添加了原型链继承的关键性代码
Son.prototype = new Father()
在上面的代码中,通过盗用构造函数的方法继承了父类实例上的属性 a
,通过原型链的方式,继承了父类的原型对象
3. 存在的问题
打印一下 son1和son2
我们将 Father
的实例绑定在了 Son
的原型上,但是我们又通过盗用构造函数的方法
将 Father
自身的属性手动添加到了 Son
的身上,因此在 Son
实例化出来的对象上,会有一个 a
属性,原型上也会有一个 a
属性
4. 优点和不足
优点:
- 解决原型链继承中属性被共享的问题
- 解决借用构造函数解决不能继承父类原型对象的问题
缺点:
- 调用了两次的父类函数,有性能问题
- 由于两次调用,会造成实例和原型上有相同的属性或方法
四、寄生组合继承
通过寄生的方式来修复组合式继承的不足,完美的实现继承
1.实现方法
在组合继承的方法中我们 call
了一次,又 new
了一次,导致调用了2次父类,而在寄生式继承中,我们可以调用 API 来实现继承父类的原型
我们将两者结合在一起
不再采用 new
关键字来给改变原型
function Father() {
this.a = [1, 2, 3, 4]
}
Father.prototype.say = function () {
console.log(111);
}
function Son() {
Father.call(this)
}
Son.prototype = Object.create(Father)
let son1 = new Son()
let son2 = new Son()
采用 Object.create
来重写子类的原型,这样就减少了对父类的调用
这时我们在控制台打印 son1
会发现问题解决了
2. 存在的问题
在这种方法中,同样存在着一些问题,当我们的子类原型上有方法时
会因为原型被重写而丢失了这些方法
我们在代码最上方添加上一个 sayHi
方法
Son.prototype.sayHi = function() {
console.log('Hi')
}
要想解决这个问题,其实可以在原型被重写之后再添加子类原型的方法
4. 优点和不足
优点:
- 基本上是最佳的继承方案了,当然还有圣杯继承
- 只调用了父类构造函数一次,节约了性能。
- 避免生成了不必要的属性
缺点:
- 子类原型被重写
五、ES6 中的继承
由于 ES6 之前的继承过于复杂,代码太多,再 ES6 中引入了一种新的继承方式 extends
继承
采用 extends
关键字来实现继承
1.实现方式
class Father {}
class Son extends Father {
constructor() {
super()
}
}
这样就实现了子类继承父类,这里的关键是需要在子类的 constructor
中添加一个 super
关键字
需要注意的是
子类中
constructor
方法中必须引用super
方法,否则新建实例会报错,这是因为子类自己的this
对象,必须先通过父类构造函数完成塑性,得到父类的属性和方法;然后再加上子类自己的属性和方法;
如果没有
super
方法,子类就没有this
对象,就会报错。