细谈Javascript中的继承以及各种继承的类型

Posted 我是真的不会前端

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了细谈Javascript中的继承以及各种继承的类型相关的知识,希望对你有一定的参考价值。

1.继承的概念

先说下原型链

原型链是实现继承的主要方法。构造函数都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针(constructor),而任何实例或者实例化对象都包含一个指向原型对象的内部指针(proto)。假如我们让原型对象A等于另一个类型B的实例,那么原型对象A就包含一个指向另一个类型B的原型对象的指针,以此类推,B的原型对象又是C类型的实例,最终指向object。上面的关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是原型链的概念。
如图所示
在这里插入图片描述

首先字面意思上简单理解就是儿子继承爸爸的特性

function Animal(name,age){
    this.name = name;
    this.age = age
}

Animal.prototype.say = function(){
    console.log('各种声音')
}

function Cat(eye) {
    this.eye = eye
}

Cat.prototype.xingwei = function () {
    console.log('抓老鼠') 
}

function Bird(body) {
    this.body = body
}

Bird.prototype.fly = function () { 
    console.log('飞') 
}

const c = new Cat('布偶')
console.log(c)
const b = new Bird('翅膀')
console.log(b)

从下面两个实例对象中可以看出,每个对象都有自己的属性和方法,不带有Animal的属性和方法,但Cat和Bird又希望能使用到Animal的属性和方法。
在这里插入图片描述

当多个构造函数需要使用一些共同的属性和方法时候,就需要将将共同的属性和方法单独封装在一个构造函数中,方便多个构造函数继承使用。
代码中的继承,其实就是让一个对象可以拥有另一个对象的属性和方法
多个对象由共同的属性和方法,就将共同的东西抽取出来进行封装并重复使用
父构造函数叫父类,子构造函数叫子类。继承的目的,就是让子类拥有父类的属性和方法。

2.原型继承

JS找构造函数的属性的时候,当找不到时就会去原型_PROTO_找。找不到再去原型的原型找。知道搜索完整个原型链。说白了原型的继承就是改原型链
通过改对象的原型链结构来实现继承,因为对象默认能访问到原型对象的属性和方法
让构造函数换一个原型。
原型继承:利用构造函数可以修改实例对象的原型,将原型换成想继承的那个对象。

function Animal(name,age){
    this.name = name;
    this.age = age
}
Animal.prototype.say = function(){
    console.log("各种声音");
}

// var a = new Animal('动物',5)
// console.log(a);


function Bird(skill){
    this.skill = skill
}

结果是:
在这里插入图片描述

// Bird.prototype = a
Bird.prototype = new Animal('动物',5)

var b = new Bird('能飞')
// 将b的原型改成Animal的实例对象a
// b.__proto__ = a 
console.log(b);
console.log(b.skill);
b.say()

优点:只要处在原型上的方法和属性都能使用
缺点:需要在两个地方传参,所有属性分另个地方存放

3.借用函数继承

借用函数(上下文调用模式):call apply bind
先说下这三个的区别:
call apply调用函数并改变其this指向
bind克隆函数并改变其this指向
利用借用函数,将父构造函数中的this改成子构造函数中的this,让子构造函数具备跟父构造函数同样的属性。

function Animal(name,age){
    this.name = name;
    this.age = age
}
Animal.prototype.say = function(){
    console.log("各种声音");
}

function Bird(skill,name,age){
    this.skill = skill
    // 这里执行Animal构造函数中的代码
    // Animal() // 直接执行,这里的this - 代表Bird的实例对象,就没有name、age
    // var that = this;
    // Animal.call(this,name,age)

    Animal.apply(this,[name,age])

}

现在我们来看结果
在这里插入图片描述
优点:解决了参数多个地方传参,存储属性在多个地方存储的问题
问题:无法继承父构造函数原型上的方法

4.混合继承

继承的方案也是个渐进的过程,直到找到最完美的方案。就跟回调函数的解决从promise的引入到async和await的方法,具体内容可以看我之前写的有关回调地狱的解决方案文章:回调地狱的终极解决方案

好了 回到正题:其实顾名思义,就是上面两种方法的混合。是将原型继承一次,借用函数再继承一次

function Animal(name,age){
    this.name = name;
    this.age = age
}
Animal.prototype.say = function(){
    console.log("各种声音");
}

function Bird(skill,name,age){
    this.skill = skill
    // 使用借用函数继承一次
    Animal.apply(this,[name,age])
}

// 使用原型再继承一次
Bird.prototype = new Animal()

// 给子类添加方法
Bird.prototype.fly = function(){
    console.log("飞的更高");
}

var b = new Bird('能飞','鸟',2)
console.log(b);



function Cat(name,age){
    this.name = name;
    this.age = age
}
Cat.prototype = new Animal()

var c = new Cat('猫',3)
console.log(c);

来看结果
在这里插入图片描述
看到这 不用我说,就能看到缺陷吧
优点:在同一个地方传递参数了,且属性都放在了一起。

缺点:父构造函数原型上的方法继承不了。

5.拷贝继承

拷贝继承是利用for in循环能将原型中的属性和方法也遍历出来的特性,将父对象中属性和方法遍历绑定到子对象的原型上。
也可以说将父类的属性拷贝到子类中。

function Animal(name,age){
    this.name = name;
    this.age = age
}
Animal.prototype.say = function(){
    console.log("各种声音");
}

function Bird(name,age){
    // 需要进行拷贝父类中的属性和方法
    // for in 循环 - 能将原型中的东西也遍历
    var a = new Animal(name,age)
    console.log(a);
    for(var attr in a){
        this.__proto__[attr] = a[attr]
    }
}

Bird.prototype.fly = function(){
    console.log("飞得更高");
}

var b = new Bird('鸟',2)
console.log(b);
console.log(b.name);

在这里插入图片描述

优点:

  1. 父类的构造函数体内的和原型上的都可以继承
  2. constructor 能正常配套
  3. 添加自己的方法的时候, 确实是在自己的原型身上

缺点:

  1. for in 循环: for in 循环需要一直遍历到 Object.prototype - 比较消耗性能
  2. 不能继承 不可枚举 的属性 - 颜色比较暗的属性和方法不能遍历到
  3. 继承来的属性不再自己身上

6.寄生继承

将父构造函数在子构造函数中实例化,然后在子构造函数中返回实例出来的对象。寄生继承写起来简单,但是不是很负责任。把子类实例化的类型返回出来。得到父对象。然后再子类将父类实例化的操作返回出来。继承出来的对象等于是把自己变成了父亲。给自己添加属性没有用。只能加到父类上去。等同于迷失了自我。

function Animal(name,age){
    this.name = name;
    this.age = age;
}

Animal.prototype.say = function(){
    console.log("各种声音");
}

function Bird(name,age,skill){
    // 给自己添加属性是没有用的,因为最终返回的不是this
    this.skill = skill
    // 在子类这个地方实例化父类得到一个对象
    var a = new Animal(name,age)
    
    // 在子类中将父类实例化出来的对象返回
    return a
}

结果
在这里插入图片描述

这种继承没有了自己的原型,原型完全是父构造函数的继承,改变自己原型后,自己原本原型上的方法又无法使用。
那么这个时候 ,就产生了改进方法
我们可以依然new父元素。我们将父原型绑在自己身上。

function Animal(name,age){
    this.name = name;
    this.age = age;
}

Animal.prototype.say = function(){
    console.log("各种声音");
}


function Bird(name,age,skill) {
    // 在子构造函数中实例化父构造函数
    var a = new Animal(name,age)
    // 改原型
    Bird.prototype = a.__proto__;
    // 给这个对象中添加属性
    a.skill = skill;
    return a;
}

Animal.prototype.fly = function(){
    console.log("飞得更高");
}


var b = Bird('鸟',2,'眼睛')
console.log(b) // 对象中又有了xingwei这个方法了

结果如下
在这里插入图片描述

但这种继承方式,将父构造函数的原型修改了,以后的继承,都会带有这个方法。但缺点还是有。就是容易产生冲突

优点:

  1. 原型和构造函数体内的都能继承下来
  2. 寄生原型的话, 自己的属性和方法依旧可以添加和使用

缺点:

  1. 寄生实例的时候, 没有自己的任何内容
  2. 寄生原型的时候, 一旦修改原型上, 父类的实例也会有这些方法

7.寄生组合继承

到现在为止,也称完美继承
进行一次寄生,再进行一次组合(原型+借用)。实际上三次继承结合到了一起。同时再加上独立的第三方构造函数。那怎么理解这个第三方呢。其实你可以理解为他是一个中间人。借助寄生和借用他拿取相应的属性。

function Animal(name,age){
    this.name = name
    this.age = age
}
Animal.prototype.say = function(){
    console.log("各种声音");
}
function Temp(name,age){
    // 通过借用函数继承父类
    Animal.call(this,name,age)
}
// 原型继承
Temp.prototype = new Animal()

// 实例化第三方类
var t = new Temp('动物',5)

// 原型继承
Bird.prototype = t


function Bird(skill){
    this.skill = skill
}
Bird.prototype.fly = function(){
    console.log("飞得更高");
}


var b = new Bird('会飞')
console.log(b);

过程如下:
在这里插入图片描述
结果如下
在这里插入图片描述

Animal经过借用函数继承后,属性已经有了,需要的只剩下原型中的方法了,所以对原型进行寄生即可,不用实例化Animal了,简化如下:

(function(){
    // 寄生组合继承:寄生继承 + 原型继承 + 借用函数继承 + 独立的第三方构造函数
    function Temp(name,age){
        // 通过借用函数继承父类
        Animal.call(this,name,age)
    }
    // 寄生原型
    Temp.prototype = Animal.prototype

    // 实例化第三方类
    // var t = new Temp('动物',5)

    // 原型继承
    Bird.prototype = new Temp('动物',5)
})()

// console.log(Temp);
function Bird(skill){
    this.skill = skill
}
Bird.prototype.fly = function(){
    console.log("飞得更高");
}


var b = new Bird('能飞')
console.log(b);

自执行函数防止变量污染。通过寄生原型直接写成Animal.prototype。
控制台结果如下
在这里插入图片描述

8.ES6中的继承

更完美的继承方案。通过class 和extends。父级的constructer需要用super来调用:
super关键字代表的就是表示通过子类的构造方法调用父类的构造方法。也可以说“当前对象”的那部分父类型特征。
super() 是调用父类的构造函数super的语法是:“super.”、“super()”。

class Animal{
    constructor(name,age){
        this.name = name
        this.age = age
    }

    say(){
        console.log("各种声音");
    }
}

class Bird extends Animal{
    constructor(name,age){
        super(name,age)
    }

    fly(){
        console.log("飞的更高");
    }
}

var b = new Bird('鸟',2)
console.log(b);

看这次的继承结果,是不是很完美
在这里插入图片描述

9.总结

说一千道一万。对于继承这件事情。无论从单纯的原型继承还是到现在ES6最完美的解决方案class+extends继承。从来都围绕不开原型。js这种弱类型本身没有面向对象。布兰登艾奇只是发明了原型来 模拟后端语言的面向对象和继承。说一千道一万,这么多种方法的改进更迭都离不开原型。JS也没有类,他用构造函数完成了类。JS本身也没有继承。同样通过原型和原型链,完成了继承。这样才实现了JS的更多高级应用。

以上是关于细谈Javascript中的继承以及各种继承的类型的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript实现类的privateprotectedpublicstatic以及继承

JavaScript实现类的privateprotectedpublicstatic以及继承

JavaScript面向对象精要

JavaScript的前世今生

javascript中的class类 以及class的继承

JavaScript之原型式继承&寄生式继承和寄生组合式继承以及优缺点