JS 中继承可以按照是否使用 object 函数(在下文中会提到),将继承分成两部分(Object.create 是ES5新增的方法,用来规范化这个函数)

其中,原型链继承和原生式继承有一样的优缺点,构造函数继承与寄生式继承也相互对应。寄生组合继承基于 Object.create, 同时优化了组合继承,成为了完美的继承方式。ES6 Class Extends 的结果与寄生组合继承基本一致,但是实现方案又略有不同

image.png

原型链继承

优点:

  • 可以继承父类原型属性/方法
  • 父类方法可以复用

缺点:

  • 父类的引用属性会被所有子类实例共享
  • 子类构建实例时不能向父类传递参数

只要把子类的 prototype 设置为父类的实例,就完成了继承

  1. // 父类
  2. function Person() {}
  3. // 子类
  4. function Student(){}
  5. // 继承
  6. Student.prototype = new Person()

继承的属性中,子类属性会覆盖父类属性,因为原型链是逐级查找的

  1. // 父类
  2. function Person(name,age) {
  3. this.name = name || 'unknow'
  4. this.age = age || 0
  5. }
  6. // 子类
  7. function Student(name){
  8. this.name = name
  9. this.score = 80
  10. }
  11. // 继承
  12. Student.prototype = new Person()
  13. var stu = new Student('lucy')
  14. console.log(stu.name) // lucy --子类覆盖父类的属性
  15. console.log(stu.age) // 0 --父类的属性
  16. console.log(stu.score) // 80 --子类自己的属性

父类方法可以进行复用

// 父类
function Person() {
  this.say = function() {};
}

// 子类
function Student() {}

// 继承
Student.prototype = new Person()

var stu1 = new Student();
var stu2= new Student();

console.log(stu1.say === stu2.say); // true

当为子类原型添加方法时,需写在继承之后,否则会被覆盖

// 父类
function Person() {}

// 为父类新曾一个方法
Person.prototype.say = function() {
  console.log('I am a person')
}

// 子类
function Student() {}

// 继承 注意:继承必须要写在子类方法定义的前面
Student.prototype = new Person()

// 为子类新增一个方法(在继承之后,否则会被覆盖)
Student.prototype.study = function () {
  console.log('I am studing')
}

但是,原型链继承有一个缺点:就是属性如果是引用类型的话,会共享引用类型

// 父类
function Person() {
  this.hobbies = ['music','reading']
}

// 子类
function Student(){}

// 继承
Student.prototype = new Person()

var stu1 = new Student()
var stu2 = new Student()

stu1.hobbies.push('basketball')

console.log(stu1.hobbies)   // music,reading,basketball
console.log(stu2.hobbies)   // music,reading,basketball

可以看到,当我们改变 stu1 的引用类型的属性时,stu2 对应的属性也会跟着更改。原因是因为引用类型是保存在堆内存中的,也就是父类中保存的是一个指针

之所以说子类构建实例时不能向父类传递参数,因为这样就失去了面向对象编程的意义,示例:

// 父类
function Parent(name){ this.name=name; }

// 子类
function Child(){}

// 继承
Child.prototype = new Parent("zs")

var person1 = new Child()
var person2 = new Child()

console.log(person1.name) // zs
console.log(person1.name) // zs

此时,Child 类就拥有了 name=“zs” 属性,而 Children 类的实例对象只能被迫接受这个 name 属性

构造函数继承

优点:

  • 父类的引用属性不会被共享
  • 子类构建实例时可以向父类传递参数

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 父类的方法不能复用,子类实例的方法每次都是单独创建的

通过改变 this 的指向,将父类构造函数的内容复制给子类构造函数

// 父类
function Person() {
  this.hobbies = ['music','reading']
}

// 子类
function Student(){
  Person.call(this)
}

var stu1 = new Student()
var stu2 = new Student()

stu1.hobbies.push('basketball')
console.log(stu1.hobbies)   // music,reading,basketball
console.log(stu2.hobbies)   // music,reading

这样,我们就解决了引用类型被所有实例共享的问题了

但这就导致了一个很矛盾的问题:函数也是引用类型,也就是说,每个实例里面的函数,虽然功能一样,但是却不是同一个函数,就相当于我们每实例化一个子类,就复制了一遍函数代码

// 父类
function Person() {
  this.say = function() {}
}

// 子类
function Student(){
  Person.call(this)
}

var stu1 = new Student()
var stu2 = new Student()
console.log(stu1.say === stu2.say)   // false

另一个好处就是可以给父类传参

// 父类
function Person(name) {
  this.name = name
}

// 子类
function Student(name){
  Person.call(this, name)
}

var stu1 = new Student('lucy')
var stu2 = new Student('lili')

console.log(stu1.name)   // lucy
console.log(stu2.name)   // lili

组合继承

优点:

  • 可以继承父类原型属性/方法
  • 父类的引用属性不会被共享
  • 子类构建实例时可以向父类传递参数

缺点:
**
调用了两次父类的构造函数,第一次给子类的原型添加了父类的属性,第二次又给子类的构造函数添加了父类的属性,从而覆盖了子类原型中的同名参数。这种被覆盖的情况造成了性能上的浪费

组合继承,就是各取上面两种继承的长处,普通属性使用构造函数继承,函数使用原型链继承

// 父类
function Person(name) {
  this.hobbies = ['music','reading'];
  this.name = name
}

// 父类函数
Person.prototype.say = function() {console.log('I am a person')}

// 子类
function Student(name){
    Person.call(this, name)        // 构造函数继承(继承属性),第二次调用
}
// 继承
Student.prototype = new Person()   // 原型链继承(继承方法),第一次调用

// 实例化
var stu1 = new Student('lucy')
var stu2 = new Student('lili')

stu1.hobbies.push('basketball')
console.log(stu1.hobbies)        // music,reading,basketball
console.log(stu2.hobbies)        // music,reading

console.log(stu1.say == stu2.say)     // true

console.log(stu1.name)   // lucy
console.log(stu2.name)   // lili

第一次调用 Person() 时,给子类的原型添加了父元素的属性,第二次调用 Person() 时给子类构造函数添加了父元素的属性。此时子类的原型和构造函数都拥有父类的属性,而方法在父类的原型中。通过原型链的关系覆盖了子类原型中的同名属性,而要想使方法可以复用,必须定义在父类的原型中,而不能定义在父类中

原型式继承

优点:父类方法可以复用

缺点:
**

  • 父类的引用属性会被所有子类实例共享
  • 子类构建实例时不能向父类传递参数

原型式继承的方法本质上是对参数对象的一个浅复制,原型式继承跟原型继承很像,两者因为都是基于prototype 继承的,所以也有一些相同的特性

function inheritObject (proto) {
  function F () {}
  F.prototype = proto
  return new F()
}

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = inheritObject(person);

ECMAScript 5 通过新增 Object.create() 方法规范化了原型式继承。这个方法接收两个参数:一 个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()inheritObject() 方法的行为相同

所以上文中代码可以转变为

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person);

寄生式继承

寄生式继承其实就是在原型式继承的基础上做了一些增强

function inheritAndStrengthen(proto){
  function F () {}
  F.prototype = proto
  let f = new F()
  f.say = function() { 
    console.log('I am a person')
  }
  return f
}

跟原型式继承比,差别就是在实例对象 f 返回之前,给f添加函数.。但是这样在实例对象上添加的引用属性,跟构造函数模式一样, 实例对象的引用类型属性无法共享

寄生组合继承

上面有提到组合继承父类构造函数里的代码会执行两遍,可以使用寄生组合继承来解决这个问题

function inheritPrototype (subType, superType) {
  var prototype = Object.create(superType.prototype) // 目的是为了继承父类原型的属性
  prototype.constructor = subType // 修正原型的构造函数(原本构造函数是 F())
  subType.prototype = prototype // 目的是为了与父类原型关联起来
}

function SuperType (name) {
  this.name = name
  this.colors = ["red", "blue", "green"]
}

SuperType.prototype.sayName = function () {
  alert(this.name)
}

function SubType (name, age) {
  SuperType.call(this, name)
  this.age = age
}
// 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
inheritPrototype(SubType, SuperType)

SubType.prototype.sayAge = function () {
  alert(this.age)
}

var test = new SubType("zs", 20)

可以参考以下的原型链

寄生组合式继承是引用类型最理想的继承范式

ES6 Class extends

ES6 继承的结果和寄生组合继承相似,本质上,ES6 继承是一种语法糖。但是,寄生组合继承是先创建子类实例 this 对象,然后再对其增强;而 ES6 先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this

class Person {
  constructor(name) {
    this.name = name
  }
  // 原型方法
  // 即 Person.prototype.getName = function() { }
  getName() {
    console.log('Person:', this.name)
  }
}

class Gamer extends Person {
  constructor(name, age) {
    // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()
    super(name) // 类似于call的继承:在这里super相当于把 Person 的constructor 给执行了,并且让方法中的 this 是 Gamer 的实例,super 当中传递的实参都是在给 Person 的 constructor 传递
    this.age = age
  }
}

const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法

参考文章