Skip to content

类的继承

作者:guo-zi-xin
更新于:1 年前
字数统计:1.8k 字
阅读时长:6 分钟

继承(inheritance)是面向对象软件技术中的一个概念

如果一个类别B '继承自' 另一个类别A,就把这个B称为'A的子类', 而把A称为 'B的父类别', 也可以称 'A是B的超类'

优点

继承可以使得子类有父类别的各种属性和方法,而不需要再次编写相同的代码

在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能

虽然 JavaScript 不是真正的面相对象语言,但天生的灵活性,使应用场景更丰富

关于继承, 我们举一个形象的例子

定义一个类(Class) 叫汽车, 汽车的属性包括颜色、轮胎、品牌、速度、排气量等等

javascript
class Car {
  constructor (color, speed) {
    this.color = color
    this.speed = speed
  }
}

由汽车的这个类可以派生出 '轿车' 和 '货车' 两个类,在汽车的基础属性上,为轿车添加一个后备箱,给货车添加一个大货箱

javascript
// 货车
class Truck extends Car {
  constructor (color, speed) {
    super(color,speed)
    this.Container = true // 货箱
  }
}

// 轿车
class MiniCar extends Car {
    constructor (color, speed) {
    super(color,speed)
    this.Trunk = true // 后备厢
  }
}

这样轿车和货车就是不一样的,但是二者都属于汽车这个类,汽车、轿车继承了汽车的属性,而不需要再次在 '轿车'中定义汽车已有的属性

在 '轿车' 继承 '汽车' 的同时,也可以重新定义汽车的某些属性,并重写或覆盖某些属性和方法,使其获得与 '汽车'这个父类不停的属性和方法

javascript
class MiniCar extends Car {
    constructor (color, speed) {
    super(color,speed)
    this.color = 'blcak' // 覆盖继承自父类别上的颜色
    this.Trunk = true // 后备厢
  }
}

实现方式

  • 原型链继承
  • 构造函数继承(借助call)
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承
原型链继承

原型链继承是比较常见的继承方式之一, 其中涉及构造函数、原型和实例。三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针

javascript
function Parent() {
  this.name = 'parent1'
  this.play = [1, 2, 3]
}

function Child() {
  this.type = 'child2'
}

Child.prototype = new Parent()
console.log(new Child())

上述代码看似没啥问题,但实际上是存在潜在问题

javascript
var s1 = new Child()
var s2 = new Child()
s1.play.push(4)
console.log(s1.play, s2.play) // [1, 2, 3, 4], [1, 2, 3, 4]

我们改变了s1play属性,会发现s2play也跟着变化了,这是因为这两个实例使用的是同一个原型对象,内存空间是共享的

构造函数继承

借助call调用 Parent函数

javascript
function Parent() {
  this.name = 'parent1'
}

Parent.prototype.getName = function () {
  return this.name
}

function Child() {
  Parent.call(this);
  this.type = 'child'
}

let child = new Child();
console.log(child) // 没问题
console.log(child.getName()) // 会报错

可以看到,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法

相比于第一种原型链继承的方法一,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法

组合继承

前面两种继承方式,各有优缺点,组合继承则是将前面两种继承方式结合继承起来

javascript
function Parent () {
  this.name = 'parent'
  this.play = [1, 2, 3]
}

Parent.prototype.getName = function () {
  return this.name
}

function Child() {
  // 第二次调用 Parent
  Parent.call(this)
  this.type = 'child'
}

// 第一次调用 Parent()
Child.prototype = new Parent()

// 手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child

var s1 = new Child()
var s2 = new Child()
s1.play.push(4)
console.log(s1.play, s2.play) // 不互相影响

console.log(s3.getName) // 正常输出 ‘parent’
console.log(s2.getName) // 正常输出 ‘parent’

这种方式看起来没什么问题,也解决了原型链继承和构造函数继承各自存在的问题, 但是从上面的代码可以看到 Parent 执行了两次,造成了多构造一次的性能开销

原型式继承

这里主要借助 Object.create 方法实现普通对象的继承

javascript
let parent = {
  name: 'parent'
  friends: ['p1', 'p2', 'p3']
  getName: function () {
    return this.name
  }
}

let person = Object.create(parent)
person.name = 'tom'
person.friends.push('jerry')

let person2 = Object.create(parent)
person.friends.push('lucy')

console.log(person.name) // tom
console.log(person.name === person.getName()) // true
console.log(person2.name); // parent
console.log(person.friends) // ['p1', 'p2', 'p3', 'jerry', 'lucy']
console.log(person2.friends) // ['p1', 'p2', 'p3', 'jerry', 'lucy']

这种继承方式的缺点也很明显,因为 Object.create 方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能

寄生式继承

寄生式继承在上面的继承基础上进行了优化,利用这个浅拷贝的能力再进行增强,添加一些方法

javascript
let parent = {
  name: 'parent',
  friends: ['p1', 'p2', 'p3'],
  getName: function () {
    return this.name
  }
}

function clone(original) {
  let clone = Object.create(original)
  clone.getFriends = function() {
    return this.friends
  }
  return clone
}

let person = clone(parent)

console.log(person.getName()) // parent
console.log(person.getFriends()) // ['p1', 'p2', 'p3']

其优缺点也很明显,跟原型式继承一样,多个实例的引用类型属性指向相同的内存,存在篡改的可能

寄生组合式继承

寄生组合式继承,借助解决普通对象的继承问题的Object.create方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式

javascript
function clone (parent, child) {
  // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
  child.prototype = Object.create(parent.prototype)
  child.prototype.consturctor = child
}

function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}

Parent.prototype.getName = function () {
  return this.name
}

function Child() {
  Parent.call(this)
  this.friends = 'child'
}

clone(Parent, Child)

Child.prototype.getFriends = function () {
  return this.friends
}

let person = new Child()
console.log(person) // { friends: 'child', name: 'child', play: [1, 2, 3], __proto__: Parent}
console.log(person.getName()) //parent
console.log(person.getFriends()) // child

可以看到 person 打印出来的结果,属性得到了继承, 方法也没有问题

文章一开头,我们使用的是ES6中的 extends关键字直接实现继承

javascript
class Person {
  constructor(name) {
    this.name = name
  }
  // 原型方法
  // 即 Person.prototype.getName = function() { }
  // 下面可以简写为 getName() { // ... }
  getName = function () {
    console.log('Person', this.name)
  }
}

class Gamer extends Person {
  constructor(name, age) {
    // 子类中存在构造函数,则需要在使用 ‘this’ 之前首先调用 super()
    super(name)
    this.age = age
  }
} 
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法

利用 babel 工具进行转换, 我们会发现 extends 实际采用的方式也是寄生组合式方式进行继承的,证明这种方式是比较好的解决继承的方式

总结

类的继承

通过 Object.create 来划分不同的继承方式,最后的寄生组合式继承方式是通过组合继承改造之后的最优的继承方式,而 extends 的语法糖和寄生组合继承的方式基本类似

文章来源

JavaScript如何实现继承
https://vue3js.cn/interview/JavaScript/inherit.html#%E4%B8%80%E3%80%81%E6%98%AF%E4%BB%80%E4%B9%88

人生没有捷径,就像到二仙桥必须要走成华大道。