js 中实现继承的几种方式及优缺点

270 阅读4分钟

一、前言

继承就是子类能访问到父类的属性及方法, 所以实现继承就是使子类能够访问到父类的方法

二、实现继承的几种方法

1. 盗用构造函数

盗用构造函数实现继承的原理是:在子函数上面执行了父函数的所有的代码,所以,通过子函数构造的每一个实例都拥有自己的继承自父函数的属性和方法,实例之间不共享引用值

  • 子类构造函数可以向父构造函数传递参数,
function Father(name) {
  this.arr = [1, 2, 3]
  this.name = name
}

function Son(name) {
  Father.call(this, name)
  this.age = 10 
  // 为确保父函数的属性不覆盖子函数的属性,子函数定义的属性在父函数调用之后执行
}

let son = new Son()

console.log(son.arr);

缺点

  • 子类不能访问父类原型上定义的方法,因为子类的原型对象并没有重写为父类的实例,子类只是执行了一遍父类的内容,其他的并没有什么关系

2. 原型链继承

原型链继承的基本原理就就是把子类的原型对象重写为父类的实例

function Father() {
  this.arr = [1, 2, 3]
}

function Son() {
  
}
// 重写子类的原型对象为父类的实例,Son.prototype.__proto__ == Father.prototype
Son.prototype = new Father()

let son1 = new Son()

son1.arr.push(4) 
let son2 = new Son()

console.log(son1.arr);  // [1, 2, 3, 4]
console.log(son2.arr);  // [1, 2, 3, 4]

缺点

  • 实例对象共享原型对象中的引用值,一个实例修改了原型对象,其他实例会继承这个变化
  • 子类在实例化时不能给父函数传递参数
  • 如图

image.png

3. 组合继承

组合继承综合了原型链和盗用构造函数两种方式,基本的实现是,通过原型链继承父类原型上的属性和方法,通过盗用构造函数继承实例属性

  • 子类的每一个实例通过盗用构造函数,把子类的构造函数扩展了,所以,所有子类构造的对象在new时,所有的属性都是实例独有的,不在实例见共享,解决了原型链中实例共享引用值的情况
  • 通过重写子类的原型为父类实例对象,从而继承了父类原型上的方法,弥补了盗用构造函数的不足
function Father(name) {
  this.name = name 
  this.array = [1, 2, 3]
}

Father.prototype.sayName = function() {
  console.log(this.name)
}

function Son(name, age) {
  Father.call(this, name)
  this.age = age
}

// 把子类的原型重新赋值我父类的实例实现继承
Son.prototype = new Father()

Son.prototype.sayAge = function() {
  console.log(this.age)
}

let instance1 = new Son('zhangsan', 30)
// push 是向自己的array属性上push元素,与其原型对象无关
instance1.array.push(4)
console.log(instance1.array)  // [1, 2, 3, 4]
instance1.sayName()
instance1.sayAge()

let instance2 = new Son('lisi', 59)
// 实例1数组改动,并没有影响实例2,
console.log(instance2.array); // [1, 2, 3]
instance2.sayName()
instance2.sayAge()

4. 原型式继承

原型式继承的使用场景是: 有一个对象,想在它的基础上创建一个新的对象,新对象能够访问到原有对象的属性和方法

function createObj(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}

let obj = {
  name: 'zhangsan',
  arr: [1, 2, 3]
}

let newObj = createObj(obj)

console.log(newObj.name); // zhangsan

缺点

  • 新创建出来的对象共享管理对象,一个对象修改了关联对象的引用值,其他对象都会改变,如图所示

image.png

  • ES5 的create方法实现同样的功能

5. 寄生式继承

寄生式继承就是在原型式继承上基础上,给新的对象添加一些方法,同样适合于已知对象,不关注构造函数的情况。

function createObj(obj) {
  let clone = Object.create(obj)
  // 增强这个对象
  clone.newFunc = function() {
    console.log(this.name);
  }
  return clone
}


let obj = {
  name: 'zhagnsa'
}

let newObj = createObj(obj)
newObj.newFunc()

缺点

  • 给对象新添加的方法难以重用,

5. 寄生组合式继承

寄生组合式继承解决了组合继承调用两次父构造函数的情况,改变了通过重写子类原型为父类实例的方式继承父类的原型方法。使用寄生式继承实现继承父类的原型方法,返回给子类原型。

function createObj(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}

function inheritPrototype(son, father) {
  // 创建原型指针指向父类原型的对象
  const prototype = createObj(father.prototype)
  // 解决由于重写原型,constructor属性丢失的问题
  prototype.constructor = son
  son.prototype = prototype 
}

function Father(name) {
  this.name = name
  this.array = [1, 2, 3]
}

Father.prototype.sayName = function() {
  console.log(this.name);
}

function Son(name, age) {
  Father.call(this, name)
  this.age = age
}

inheritPrototype(Son, Father)

Son.prototype.sayAge = function() {
  console.log(this.age);
}

如图所示

image.png

6. ES6 class继承

  class Person {
    constructor(name, age) {
      this.name = name
      this.age = age
    }
    sex = '男' 
    // 定义在原型对象上 
    getName() {
      return this.name
    }
    // 定义在类名上
    static getAge() {
      console.log(123);
    }
  }

  let zhangsan = new Person('zhangsan', 10)

  class Student extends Person{
    constructor(name, age, studentNum) {
      // super相当于调用了父类的构造函数,super只能在子类的 constructor和静态方法中使用
      // 如果子类没有定义constructor方法,则子类自动执行super继承来自父类的属性和方法
      // 如果定义了构造函数,就必须写上super继承父类方法
      super(name, age, studentNum)
      this.studentNum = studentNum
    }
  }


let stu1 = new Student('name', 'age', '')

console.log(stu1
);
console.log(stu1.getName());

参考文献 《javascript高级程序设计》