【JS基础系列】5种继承方案

今天是系列第四篇,主要讲一下继承相关的问题。我发现上周原型链部分还有几个概念没有说清楚,为了避免影响继承知识点的学习,我决定先把上周原型链中的prototype、constructor和__proto__这几个概念再作一下补充,也当作是前期回顾吧。es6

prototype是什么?

prototype对象用于存放同一类型实例的共享属性和方法,目的是为了减小内存消耗。举个生活中的例子来理解这个概念:咱们每个家庭都有购物和治病的需求,可是不可能每一个家庭都建造一个超市和医院,这样会形成很大的资源浪费。现代化作法是在公共区域创建一个能够共用的超市和医院,知足全部当地人的须要,这样让人们获得了实惠,资源也被很好的使用了。以下图:
prototype图解app

constructor是什么?

constructor就是一个指向自身构造函数引用的属性。通常存在对象.constructor === 构造函数,这个概念在接下来的继承中会有涉及。而且constructor其实是被当作共享属性放在它的原型对象中。因此咱们能够看作prototype.constructor === 构造函数,这个对象就是当前构造函数的原型。以下图:
constructor图解函数

__proto__是什么?

我以前看到过一个结论,即:构造函数.prototype.constructor === 实例对象.__proto__.constructor === 构造函数。
咱们已经知道了原型和构造函数之间的关系,如今能够看做有了实例对象以后怎么跟原型产生一种关系来实现与构造函数之间的联系。那么咱们可能会设置一个属性指向原型,那么这个属性就是__proto__。
有了这个基础咱们就能得出下面的结论:post

实例对象.__proto__ === 构造函数.prototype
实例对象.__proto__.constructor === 构造函数

若是运用原型链的知识,还有以下结论:学习

实例对象.constructor === 实例对象.__proto__.constructor

查找对象属性时会先看当前对象是否存在该属性,若是不存在则会去其原型链上找,若是原型链上没有,则返回undefined。实例对象不存在constructor属性,则会去原型链上找,因此和实例对象.__proto__.constructor找的是同一个值。
__proto__的基本解释以下图:
__proto__图解this

有了上面的知识以后,能够梳理继承的知识了。如下是市面上常见的5种继承方式:spa

  • 原型链继承
  • 借用构造函数继承
  • 组合式继承
  • 寄生组合式继承
  • es6继承

原型链继承

  • 优势:可以继承父类的原型方法
  • 缺点:prototype

    • 不能给超类型的父类传参(即便传参了,全部的实例获得的属性是同一个,例子见demo1)
    • 超类型的父类属性是引用类型时,该属性会被全部实例共享(由于继承父类的全部属性都是共享属性,全部实例访问到的这个属性都是同一个内存空间,例子见demo2)
// demo1
function Parent(name){
    this.name = name
}
function Son(){}
Son.prototype = new Parent('凉凉')
Son.prototype.constructor=Son
const son1 = new Son()
const son2 = new Son()
console.log(son1.name, son2.name) // 凉凉 凉凉


// demo2
function Parent(){
    this.animals = ['老虎', '狮子']
}
function Son(){}
Son.prototype = new Parent()
Son.prototype.constructor=Son
const son1 = new Son()
const son2 = new Son()
son1.animals.push('猴子')
son2.animals.push('猪')
console.log(son1.animals) // ["老虎", "狮子", "猴子", "猪"]

本质是:将父类的实例赋值给子类的原型,让子类的原型拥有父类的全部属性和原型。可是原型上的全部属性都是共享的,因此任何一个子类实例修改了原型中的属性,其余实例获取到的属性值也会引发变化。
另外还注意一点:上面的例子Son.prototype.constructor在默认状况下是指向函数Parent,因此须要从新设置一下指向Son.prototype.constructor=Soncode

借用构造函数继承

  • 优势:解决父类属性是引用类型被全部实例共享的问题和给子类传参的问题
  • 缺点:不能继承父类超类型的原型方法

看例子:对象

function Parent(name){
    this.name = name
    this.animals = ['老虎', '狮子']
}
Parent.prototype.getName = function(){
    return this.name
}
function Son(name){
    Parent.call(this, name)
}
const son1 = new Son('小李')
const son2 = new Son('小王')
son1.animals.push('猴子')
son2.animals.push('猪')
console.log(son1.name) // 小李
console.log(son1.animals) //  ["老虎", "狮子", "猴子"]
console.log(son1.getName()) // throw error

本质是执行了一遍父类的构造函数,并让父类构造函数的this指向子类构造函数的this(即this指向子类的实例),因此子类的实例拥有和父类实例同名属性,可是没有继承原型对象。

组合式继承

  • 优势:集合了原型继承和借用构造函数继承的优势
  • 缺点:父类构造函数会执行两遍。子类的原型对象和原型链中会出现两个相同的同名属性

看例子:

function Parent(name){
    this.name = name
    this.animals = ['老虎', '狮子']
}
Parent.prototype.getName = function(){
    return this.name
}
function Son(name){
    Parent.call(this, name)
}
Son.prototype = new Parent()
const son1 = new Son('小李')
const son2 = new Son('小王')
son1.animals.push('猴子')
son2.animals.push('猪')
console.log(son1.name) // 小李
console.log(son1.animals) //  ["老虎", "狮子", "猴子"]
console.log(son1.getName()) // 小李

从结果的输出来看挺完美。可是在继承的过程当中Parent函数执行了两次,而且子类的原型对象和原型链中会出现两个相同的同名属性。由于原型链继承和借用构造函数继承都分别执行了一次。
说到这里我有一个疑问:为何组合继承可以把上面两个继承的优势都发挥出来呢?
答:借用构造函数的方式会在子类的实例对象上建立父类的同名属性,原型链继承的方式会在子类的原型上拥有父类的属性和原型。可是在访问某个对象的属性时,会先在当前对象中找有没有该属性,若是不存在就会去它的原型上找。因此会先去找经过call/apply绑定在当前对象上的属性,而不是原型中的共享属性。因此能够获取到子类实例的属性和原型。

为了解决父类构造函数执行两次的问题,又推出了寄生组合继承方法。

寄生组合式继承

  • 优势:下降调用父类构造函数的开销,只调用父类构造函数一次(原理是:将原型链继承的那部分进行改造)
  • 缺点: /

看例子:

function Parent(name){
    this.name = name
    this.animals = ['老虎', '狮子']
}
Parent.prototype.getName = function(){
    return this.name
}
function Son(name){
    Parent.call(this, name)
}

Son.prototype = Object.create(Parent.prototype)
Son.prototype.constructor = Son

const son1 = new Son('小李')
const son2 = new Son('小王')
son1.animals.push('猴子')
son2.animals.push('猪')
console.log(son1.name) // 小李
console.log(son1.animals) //  ["老虎", "狮子", "猴子"]
console.log(son1.getName()) // 小李

实质是:经过Object.create(obj)建立一个原型是obj的空对象赋值给子类的原型。还有一点须要注意:全部基于原型链继承的都须要记住constructor的指向问题,寄生继承至关因而原型链继承的一种变形。

基于原型链的继承若是constructor没有从新设置指向的话,它指向的是超类型构造函数。由于constructor是原型的一个共享属性,因此在子类原型中查找constructor属性时其实会在原型链上去找constructor指向的值,最后指向了超类型构造函数。看例子:

function Parent(){}
function Son(){}
Son.prototype = new Parent()
console.log(Son.prototype.constructor) // Parent

es6继承

最后一个是经过class,extends关键字实现继承的方式。es6继承具体函数和关键字的做用是什么,下一篇文章会单独拎出来说,先看一个class继承的例子:

class Parent{
    constructor(name){
        this.name = name
    }
    getName(){
        return this.name
    }
}

class Son extends Parent{
    constructor(name, age){
        super(name)
        this.age = age
        console.log(new.target)
    }
    introduce(){
        return `我叫作${this.name},今年${this.age}岁了`
    }
}
const s = new Son("小李", 8)
console.log(s.introduce()) // 我叫作小李,今年8岁了
console.log(s.getName()) // 小李

总结

  • 原型继承

    • 优势:可以继承父类的原型方法
    • 缺点:

      • 不能给超类型的父类传参
      • 超类型的父类属性是引用类型时,该属性会被全部实例共享
  • 借用构造函数的继承

    • 优势:解决父类属性是引用类型被全部实例共享的问题和给子类传参的问题
    • 缺点:不能继承父类超类型的原型方法
  • 组合继承

    • 优势:集合了原型继承和借用构造函数继承的优势
    • 缺点:父类构造函数会执行两遍。子类的原型对象和原型链中会出现两个相同的同名属性
  • 寄生组合式继承

    • 优势:下降调用父类构造函数的开销,只调用父类构造函数一次(原理是:将原型链继承的那部分进行改造)
    • 缺点: /
  • es6继承

    • 优势:引入了类的概念,使用较多语法糖,让继承书写更贱简单灵活
    • 缺点: /

参考文章

相关文章
相关标签/搜索