8月更文挑战 | JS中八种继承方案你知道几种?

366 阅读6分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

查漏补缺js中八种继承方案,依次介绍现有的几种继承方案并且会进行代码实操以及继承的优劣对比

原型继承

在js中每个实例对象都有一个属性__proto__来指向构造函数的原型对象prototype,而原型继承主要也是通过js中的原型对象来实现继承,Son方法的prototype指向了Person的实例对象,继承的本质实际就是重写原型对象

function Person() { //创建一个父函数
  this.name = '令狐冲'
  this.opt = [
    '男',
    20
  ]
}
​
Person.prototype.getName = function () {
  return this.name
}
​
function Son() {//子函数
  this.name = '张无忌'
} 
​
Son.prototype = new Person() //子函数通过原型继承父类实例Son.prototype.getName = function () {
  return this.name
}
​
const son1 = new Son()
​
son1.opt.push('吃')
​
const son2 = new Son()
​
console.log(son1.getName()) //张无忌//可以看到改变了son1同时也影响了son2
console.log(son1.opt) // ['男',20,'吃']
console.log(son2.opt) // ['男',20,'吃']

原型对象的优点是可以复用原型上定义的属性,这对于普通类型的值是没问题的,但是缺点是对于定义的引用类型,在代码中可以看到多个实例有可能会篡改引用类型而导致其他实例对象引用错误

借用构造函数继承

借用构造函数继承主要利用了this绑定的方法来实现继承,主要原理是在子类的构造函数中使用call或者apply调用父类的构造函数,所以说相当于将Person内部this上的属性和方法复制了一遍到实例对象上

function Person(){
  this.name = '东方不败'
  this.opt = ['吸功大法']
}   
​
Person.prototype.getName = function (){
  return this.name
}
​
function Son(){
  Person.apply(this)
  this.name = '任盈盈'
}   
​
const son1 = new Son()
const son2 = new Son()
​
son1.opt.push('九阳神功')
​
console.log(son1.name) //任盈盈
console.log(son1.getName) //undefined
console.log(son1.opt) //['吸功大法','九阳神功']
console.log(son2.opt) //['吸功大法']

借用构造函数的优势是不会像原型继承一样因为篡改引用类型导致其他实例对象错误,但是同样因为没有使用原型继承所以不能复用父类原型上的方法和属性,并且每一个实例都是需要拷贝一份父类中构造函数的方法作为自己的方法,这样对于内存占用比较大

组合继承

组合继承是原型继承和借用构造函数继承的组合版,继承了上面两个的优良传统,可以解决复用父级上面的方法,并且也不会因为多个实例造成引用类型混乱

function Person() {
  this.name = 'xxx'
  this.age = 20
  this.opt = ['降龙十八掌']
}

Person.prototype.getName = function () {
  return this.name
}
​
function Son(){
  Person.call(this)
  this.name = '周芷若'
}
​
Son.prototype = new Person()
​
Son.prototype.constructor = Sonconst son1 = new Son()
​
son1.opt.push('九阴白骨爪')
​
const son2 = new Son()
​
console.log(son1.opt) // ['降龙十八掌','九阴白骨爪']
console.log(son2.opt) // ['降龙十八掌']
console.log(son1.getName()) // 周芷若

组合继承拥有了前两个继承的优点但是也有一个缺点,在第一次调用Person的时候在Son本身添加了一次,第二次调用的时候在Son原型对象上又添加了一次,这样到最后实例上的属性会覆盖原型对象上的同名属性相当于复制了两份父级的属性在Son上面

compress.png

原型式继承

原型式继承将父级的原型对象(prototype)复制给一个新函数的prototype之后实例化返回,这个类似于一个浅拷贝和Object.create相似

function Person(){
  this.name = '张三丰'
  this.age = 100
}
​
Person.prototype.getName = function (){
  return this.name
}
​
function Change(obj){
  function Fn(){}
  Fn.prototype = obj
  return new Fn()
}
​
const P1 = {
  name:'翠山'
}
​
let son1 = Change(Person.prototype)
​
let son2 = Change(P1)
​
console.log(son1.name) //undefined
console.log(son1.getName) //function
console.log(son2.name) //翠山

这个并没有什么优点,缺点就是不能向父类构造函数传参数,并且在父类是构造函数的情况下拿不到父类上的属性只能使用原型对象的属性,如果父级是个对象还可以共享属性

寄生继承

与寄生构造函数和工厂模式类似,创建一个用于封装继承过程的函数,函数在内部以某种方式来增强对象,最后返回对象

function Person(obj) {
  let result = Object.create(obj)
  result.getName = function () {
    console.log(this.name)
  }
  return result
}
​
let data = {
  name: '白自在'
}
​
const son = Person(data)
​
son.getName() //白自在

也没啥优点缺点和原型式继承类似都不能向父构造函数传参数(感觉没啥用啊🤔)

寄生式组合继承

寄生式组合继承主要集成了寄生式继承和借用构造函数继承的优良传统,通过借用构造函数继承取到了Person上的属性,通过寄生式继承可以做到复用原型对象上的属性和方法

function Extends(Person, Son) {
  let prototype = Object.create(Person.prototype) //创建父类的原型对象
  prototype.constructor = Son //保持构造函数的准确性
  Son.prototype = prototype //原型对象继承
}
​
function Person() {
  this.name = '谢烟客'
  this.opt = ['太玄经']
}
​
Person.prototype.getName = function(){
  return this.name
}
​
function Son(){
  Person.call(this) //只需要调用一次父级
  this.name = '不三不四'
}
​
Extends(Person, Son)
​
const son1 = new Son()
​
son1.opt.push('天山折梅手')
​
const son2 = new Son()
​
console.log(son1.opt) //['太玄经','天山折梅手']
console.log(son2.opt) //['太玄经']
console.log(son1.name) //不三不四
console.log(son1) //看下图

寄生式组合继承现在来说是比较成熟的一种继承方法,这样也不会像组合继承复制两份属性占用额外内存,也可以正常的像构造函数传参,对于引用类型的安全也可以得到控制,并且现在流行的Es6的继承原理也是这样

WechatIMG1.png

混入方式继承多个父级对象

混入方式继承是由多个对象组成的父级,之后子级去继承主要用到了Object.assign合并对象

function Person() {
  this.name = '郭靖'
  this.age = 300
  this.opt = ['左右互搏']
}
​
Person.prototype.getName = function (){
  return this.name
}
​
function Person1() {
  this.name = '丁春秋'
  this.opt = ['化功大法']
}
​
Person1.prototype.getOpt = function (){
  return this.opt
}
​
function Son() {
  Person.call(this)
  Person1.call(this)
  this.name = '杨过'
}
​
Son.prototype = Object.create(Person.prototype)
​
Son.prototype = Object.assign(Son.prototype, Person1.prototype)
​
Son.prototype.constructor = Sonconst son1 = new Son()
const son2 = new Son()
​
son1.opt.push('凌波微步')
​
console.log(son1.opt) //["化功大法", "凌波微步"]
console.log(son2.opt) //["化功大法"]
console.log(son1.getOpt()) //["化功大法", "凌波微步"]
console.log(son1.getName()) // 杨过
console.log(son1)//看下图

混入继承方式的好处就是可以继承多个父级对象注意后面继承的父级的属性是会覆盖前一个父级的属性,因为主要使用的合并对象的方法

333.png

Es6方式extends继承

extends继承使用的是class类的方式也是目前使用最多的一种,创建一个父的class类之后子class类通过extends继承,注意如果子类要写constructor必须要调用super,不写constructor的情况super是默认调用的

class Person {
  name = '风陵师太'
  opt = ['黯然销魂掌']
​
  getName(){
    return this.name
  }
}
​
class Son extends Person{
  constructor(){
    super()
    this.name = '杨过'
  }
}
​
const son1 = new Son()
const son2 = new Son()
​
son1.opt.push('太极')
​
console.log(son1.opt) //["黯然销魂掌", "太极"]
console.log(son2.opt) //["黯然销魂掌"]
console.log(son1.getName()) //杨过
console.log(son1) //看下图

可以看到最后son1的样子和寄生式组合继承基本是一样的,class继承的方式优点很多便于开发者理解上手容易浅显易懂,但是里面也有一些自己的使用规则我们看总结

4444.png

总结一下

  • class继承方式

    1、浅显易懂,方便维护

    2、内部自动开启严格模式

    3、class声明不会有变量提升并且class只能用new调用

    4、内部定义的所有方法,默认都是不可枚举的

    5、继承主要依赖extends关键字完成

    6、因为子类没有自己的this对象,所以必须先调用父类的super()方法(如果不写constructor默认调用super)

    7、super的原理是调用父类的构造函数或者直接当对象调用父类中的属性

  • 普通继承

    1、晦涩难懂学习成本高

    2、继承方式很多但是比较靠谱的就几种

    3、ES5的继承是先创建子类的实例对象,然后将父类的方法添加到this上(发生在实例化的过程中)

结束

点赞富三代,评论美一生,周末愉快☕️