JS 八种常用继承

定义

继承就是子类可以使用父类的所有功能,并且对这些功能进行扩展。

八种继承方式

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"}
复制代码

image.png
将子类的原型对象指向父类的实例,这种方式就叫做原型链继承。

伪代码:

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"}
复制代码

介绍

在子类构造函数内部使用callapply来调用父类构造函数。

伪代码:

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
复制代码

image.png

关于constructor

它是构造函数原型对象中的一个属性,正常情况下它指向的是原型对象。并不会影响任何JS内部属性,只是用来标示一下某个实例是由哪个构造函数产生而已。

image.png

总结

优点

  • 可以继承父类实例的属性和方法,也能够继承父类原型属性和方法
  • 弥补了原型链继承中引用属性共享的问题
  • 可传参,可复用

缺点

  • 使用组合继承时,父类构造函数会被调用两次
  • 并且生成了两个实例,子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,所以增加了不必要的内存

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
复制代码

image.png

总结

寄生组合继承算是ES6之前一种比较完美的继承方式。
拥有上述所有继承方式的优点:

  • 只调用了一次父类构造函数,只创建了一份父类属性
  • 子类可以用到父类原型链上的属性和方法
  • 能够正常使用intanceOfisPrototypeOf方法

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
复制代码

image.png

8.class中的继承

介绍

class中继承主要靠两个东西——extendssyuper,该继承效果和寄生组合继承方式一样。

伪代码:

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继承

image.png

寄生组合继承

image.png

可以看到,class的继承方式完全满足于寄生组合继承。

extends作用:

  • class可以通过extends关键字实现继承父类的所有属性和方法
  • 若是使用了extends实现继承的子类内部没有constructor方法,则会被默认添加constructorsuper

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实现继承不一定要constructorsuper,因为没有的话会默认产生并调用了它们。

extends后面接的目标不一定是class,只要是个有prototype属性的函数就可以。

  • super相关

super有两种调用方式:当成函数调用、当成对象调用。

super当成函数调用时,代表父类的构造函数,且返回的是子类的实例,也就是此时的super内部this指向子类。在子类的constructorsuper()就相当于是Parent.contrustor.call(this)

super当成对象调用时,普通函数中super对象指向父类的原型对象,静态函数中指向父类。且通过super调用父类方法时,super会绑定子类的this,就相当于是Parent.prototype.fn.call(this)

参考文章:juejin.cn/post/684490…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享