定义
继承就是子类可以使用父类的所有功能,并且对这些功能进行扩展。
八种继承方式
1.原型链继承
介绍
构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。
继承的本质就是复制,即重写原型对象,代之以一个新类型的实例。
案例
function Parent () {
this.name = 'Parent'
this.sex = 'boy'
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child () {
this.name = 'child'
}
Child.prototype = new Parent()
var child1 = new Child()
child1.getName()
console.log(child1)
// 打印出
// 'child'
// Child{name:"child"}
复制代码
将子类的原型对象指向父类的实例,这种方式就叫做原型链继承。
伪代码:
Child.prototype = new Parent()
复制代码
而非将子类的原型对象指向父类的原型对象。
Child.prototype = Parent.prototype
复制代码
总结
优点:
继承了父类的模板,又继承了父类的原型对象
缺点:
- 如果要给子类的原型上新增属性和方法,就必须放在
Child.prototype = new Parent()
这样的语句后面。 - 无法实现多继承,因为已经制定了原型对象了。
- 来自原型对象的所有属性都被共享了,这样如果不小心修改了原型对象中的引用类型属性,那么所有子类创建的实例对象都会受到影响。
- 创建子类时,无法向父类构造函数传参数。
2.构造继承
案例:
function Parent (name) {
this.name = name
}
function Child () {
this.sex = 'boy'
Parent.call(this, 'child')
}
var child1 = new Child()
console.log(child1)
// 打印出
// Child {sex: "boy", name: "bad boy"}
复制代码
介绍
在子类构造函数内部使用call
或apply
来调用父类构造函数。
伪代码:
function Child () {
Parent.call(this, ...arguments)
}
复制代码
总结
优点:
解决了原型链继承中子类实例共享父类引用对象的问题,实现了多继承,创建子类实例时,可以向父类传递参数。
缺点:
- 构造只能继承父类的实例属性和方法,不能继承父类原型的属性和方法。
- 实例并不是父亲的实例,只是子类的实例。
- 无法实现函数复用,每个子类都有父类实力函数的副本,影响性能。
3.组合继承
介绍
组合继承就是将原型链继承与构造函数继承组合在一起,从而发挥两者之长的一种继承模式。
思路:
- 使用原型链继承来保证子类能继承到父类原型中的属性和方法
- 使用构造继承来保证子类能继承到父类的实例属性和方法
例子
function Parent(name){
console.log(name)
this.name = name
}
function Child(name){
Parent.call(this,name)
}
Child.prototype = new Parent()
var child1 = new Child('child')
console.log(child1)
console.log(Child.prototype)
// 打印出
// undefined
// 'child1'
// Child{name:'child1'}
// Parent{name:undefined}
复制代码
虽然只调用了new Child()
一次,但是在Parent
中却两次打印出了name
。
第一次是原型链继承的时候,new Parent()
。
第二次是构造继承的时候,Parent.call()
调用的。
也就是说,在使用组合继承的时候,会凭空多调用一次父类构造函数。
伪代码:
// 构造继承
function Child () {
Parent.call(this, ...arguments)
}
// 原型链继承
Child.prototype = new Parent()
// 修正constructor
Child.prototype.constructor = Child
复制代码
关于constructor
它是构造函数原型对象中的一个属性,正常情况下它指向的是原型对象。并不会影响任何JS内部属性,只是用来标示一下某个实例是由哪个构造函数产生而已。
总结
优点
- 可以继承父类实例的属性和方法,也能够继承父类原型属性和方法
- 弥补了原型链继承中引用属性共享的问题
- 可传参,可复用
缺点
- 使用组合继承时,父类构造函数会被调用两次
- 并且生成了两个实例,子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,所以增加了不必要的内存
4.寄生组合继承
介绍
例子
Object.create(proto,propertiesObject)
参数一:需要指定的原型对象
参数二:可选参数,给新对象自身添加新属性以及描述器
function Parent (name) {
this.name = name
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name) {
this.sex = 'boy'
Parent.call(this, name)
}
// 与组合继承的区别
Child.prototype = Object.create(Parent.prototype)
var child1 = new Child('child1')
console.log(child1)
child1.getName()
console.log(child1.__proto__)
console.log(Object.create(null))
console.log(new Object())
复制代码
可以看到,寄生组合继承与组合继承的区别仅仅是Child.prototype
不同。
伪代码:
// 构造继承
function Child () {
Parent.call(this, ...arguments)
}
// 原型式继承
Child.prototype = Object.create(Parent.prototype)
// 修正constructor
Child.prototype.constructor = Child
复制代码
总结
寄生组合继承算是ES6之前一种比较完美的继承方式。
拥有上述所有继承方式的优点:
- 只调用了一次父类构造函数,只创建了一份父类属性
- 子类可以用到父类原型链上的属性和方法
- 能够正常使用
intanceOf
和isPrototypeOf
方法
5.原型式继承
介绍
创建一个构造函数,构造函数的原型指向对象,然后调用new操作符创建实例,并返回这个实例,本质是一个浅拷贝。
原理:
function objcet (obj) {
function F () {};
F.prototype = obj;
F.prototype.constructor = F;
return new F();
}
复制代码
在ES5之后可以直接使用Object.create()
方法来实现。
伪代码:
var child = Object.create(parent)
复制代码
总结
优点
- 在不用创建构造函数的情况下,实现了原型链继承,代码量减少一部分。
缺点
- 一些引用数据操作的时候会出问题,两个实例会公用继承实例的引用数据类型。
- 谨慎定义方法,以免定义方法也继承对象原型的方法重名。
- 无法直接给父级构造函数使用参数。
6.寄生式继承
介绍
在原型式继承的基础上再封装一层,来增强对象,之后将这个对象返回。
function createAnother (original) {
var clone = Object.create(original);; // 通过调用 Object.create() 函数创建一个新对象
clone.fn = function () {}; // 以某种方式来增强对象
return clone; // 返回这个对象
}
复制代码
总结
优点
在不用创建构造函数的情况下,实现了原型链继承,代码量减少以部分。
缺点
- 一些引用数据操作的时候会出问题,两个实例会公用继承实例的引用数据类。
- 谨慎定义方法,以免定义方法也继承对象原型的方法重名。
- 无法直接给父级构造函数使用参数。
7.混入式继承
介绍
一个子类继承多个父类,可以把多个对象的属性和方法拷贝到目标对象中,若是存在同名属性的话,后面的会覆盖到前面。
function Child () {
Parent.call(this)
OtherParent.call(this)
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype)
Child.prototype.constructor = Child
复制代码
8.class中的继承
介绍
在class
中继承主要靠两个东西——extends
、syuper
,该继承效果和寄生组合继承方式一样。
伪代码:
class Child extends Parent{
constructor(...args){
super(...args)
}
}
复制代码
extends
重写4.寄生组合继承的例子:
function Parent (name) {
this.name = name
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name) {
this.sex = 'boy'
Parent.call(this, name)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
var child1 = new Child('child1')
console.log(child1)
child1.getName()
console.log(child1 instanceof Child)
console.log(child1 instanceof Parent)
// 打印出
// Child{ name: 'child1', sex: 'boy' }
// 'child1'
// true
// true
复制代码
用calss继承改写:
class Parent {
constructor (name) {
this.name = name
}
getName () {
console.log(this.name)
}
}
class Child extends Parent {
constructor (name) {
super(name)
this.sex = 'boy'
}
}
var child1 = new Child('child1')
console.log(child1)
child1.getName()
console.log(child1 instanceof Child)
console.log(child1 instanceof Parent)
// 打印出
// Child{ name: 'child1', sex: 'boy' }
// 'child1'
// true
// true
复制代码
让我们对比一下返回的结果:
class
继承
寄生组合继承
可以看到,class
的继承方式完全满足于寄生组合继承。
extends
作用:
class
可以通过extends
关键字实现继承父类的所有属性和方法- 若是使用了
extends
实现继承的子类内部没有constructor
方法,则会被默认添加constructor
和super
。
super
例子
class Parent {
constructor () {
this.name = 'parent'
}
}
class Child extends Parent {
constructor () {
// super(name) // 把super隐去
}
}
var child1 = new Child()
console.log(child1)
child1.getName()
// 报错
复制代码
如果不使用super
,就会报错,所以必须得在constructor
中调用super
函数。
这其实和ES6的继承机制有关:
在ES5中的继承(例如构造继承、寄生组合继承),实质上是先创造子类的实例对象this
,然后再将父类的属性和方法添加到this
上(使用的是Parent.call(this)
)。
而在ES6中,实质是先创造父类的实例对象this
(也就是使用super()
),然后再用子类的构造函数去修改this
。
所以class
继承中,子类必须得在constructor
中调用super
方法,否则新建实例就会报错,因为子类自己没有自己的this
对象,而是继承父类的this
对象,然后对其加工,如果不调用super
的话,子类就得不到this
对象。
总结
- ES6中的继承
使用了extends
实现继承不一定要constructor
和super
,因为没有的话会默认产生并调用了它们。
extends
后面接的目标不一定是class
,只要是个有prototype
属性的函数就可以。
- super相关
super
有两种调用方式:当成函数调用、当成对象调用。
super
当成函数调用时,代表父类的构造函数,且返回的是子类的实例,也就是此时的super
内部this
指向子类。在子类的constructor
中super()
就相当于是Parent.contrustor.call(this)
。
super
当成对象调用时,普通函数中super
对象指向父类的原型对象,静态函数中指向父类。且通过super
调用父类方法时,super
会绑定子类的this
,就相当于是Parent.prototype.fn.call(this)
。