• JS 继承


    一、继承是什么?

    继承(inheritance)是面向对象软件技术当中的一个概念。
    如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”

    • 继承的优点:

    继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码

    在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能

    虽然JavaScript并不是真正的面向对象语言,但它天生的灵活性,使应用场景更加丰富

    关于继承,我们举个形象的例子:

    定义一个类(Class)叫汽车,汽车的属性包括颜色、轮胎、品牌、速度、排气量等

    class Car{
        constructor(color,speed){
            this.color = color
            this.speed = speed
            // ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    由汽车这个类可以派生出“轿车”和“货车”两个类,在汽车的基础属性上,为轿车添加一个后备厢、给货车添加一个大货箱

    // 货车
    class Truck extends Car{
        constructor(color,speed){
            super(color,speed)
            this.Container = true // 货箱
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样轿车和货车就是不一样的,但是二者都属于汽车这个类,汽车、轿车继承了汽车的属性,而不需要再次在“轿车”中定义汽车已经有的属性

    在“轿车”继承“汽车”的同时,也可以重新定义汽车的某些属性,并重写或覆盖某些属性和方法,使其获得与“汽车”这个父类不同的属性和方法

    class Truck extends Car{
        constructor(color,speed){
            super(color,speed)
            this.color = "black" //覆盖
            this.Container = true // 货箱
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    从这个例子中就能详细说明汽车、轿车以及卡车之间的继承关系

    二、继承实现的方式

    2.1 原型链继承

    原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针

    function Person() {
      this.name = 'aaa';
      this.age = 18;
      this.friends = [];
    }
    // 原型上添加方法
    Person.prototype.eat = function () {
      console.log(this.name + 'eating');
    }
    
    
    function Student() {
      this.sno = 111;
    }
    // 给student构造函数上继承一个父类
    Student.prototype = new Person();
    // 原型添加方法
    Student.prototype.studying = function () {
      console.log(this.name + 'studying');
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    上面代码看似没问题,实际存在潜在问题

    
    let stu = new Student()
    let stu2 = new Student()
    
    // 弊端一: 打印实例,无法获取到继承到数据
    console.log(stu);
    
    // 弊端二: 给父类friends添加kobe,stu和stu2都会打印出kobe,原因为给父类添加的数据,索引两个实例都会在父类中查找到
    stu.friends.push('kobe')  // 修改值 会修改原型上的 friends
    // stu.name = 'xyh'          // 赋值 不会修改原型上的name,而是在实例上创建name
    console.log(stu.friends);
    console.log(stu2.friends);
    
    // 弊端三: 不能传递参数
    let stu3 = new Student('jlc', 19, ['wyc'], 333)
    console.log(stu3);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    解决方式:

    我们只需要在student构造函数中添加这行代码即可

    function Student(name, age, friends, sno) {
    	Person.call(this, name, age, friends); // 继承父类的方法  将student中的this传递给person中去继承属性
    	this.sno = sno;
    }
    
    • 1
    • 2
    • 3
    • 4

    更新后的完整代码:

    function Person(name, age, friends) {
      this.name = name;
      this.age = age;
      this.friends = friends;
    }
    Person.prototype.eat = function () {
      console.log(this.name + 'eating');
    }
    
    
    function Student(name, age, friends, sno) {
      Person.call(this, name, age, friends); // 继承父类的方法  将student中的this传递给person中去继承属性
      this.sno = sno;
    }
    // 给 人  赋值给 学生构造函数
    Student.prototype = new Person();
    
    Student.prototype.studying = function () {
      console.log(this.name + 'studying');
    }
    
    let stu = new Student('wyc',19, ['xyh'], 111)
    let stu2 = new Student('xyh',16, ['wyc'], 222)
    
    // 解决弊端一: 打印实例,无法获取到继承到数据
    console.log(stu);
    
    // 解决弊端二: 给父类friends添加kobe,stu和stu2都会打印出kobe,原因为给父类添加的数据,索引两个实例都会在父类中查找到
    stu.friends.push('kobe')  // 修改值 会修改原型上的 friends
    // stu.name = 'xyh'          // 赋值 不会修改原型上的name,而是在实例上创建name
    console.log(stu.friends);
    console.log(stu2.friends);
    
    // 解决弊端三: 不能传递参数
    let stu3 = new Student('jlc', 19, ['wyc'], 333)
    console.log(stu3);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    2.2 原型式继承

    新对象的原型指向 obj 对象

    三种实现:

    let obj = {
      name: 'wyc',
      age: 19,
      friends: ['xhy']
    }
    
    // 实现方式一
    function createObject(o) {
      var newObj = {}
      Object.setPrototypeOf(newObj, o)
      return newObj;
    }
    // 实现方式二
    function createObject2(o) {
      function foo() {}
      foo.prototype = o;
      let newObj = new foo()
      return newObj;
    }
    // let fn = createObject2(obj);
    // 实现三
    let fn = Object.create(obj)
    let fn1 = Object.create(obj)
    
    
    // 向基本数据类型添加数据
    fn.name = 'xyh'
    // 向引用数据类型添加数据
    fn.friends.push('jlc')
    
    console.log(fn);
    console.log(fn.__proto__);
    console.log(fn1);
    console.log(fn1.__proto__);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    这种继承方式的缺点也很明显,因为Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能

    在这里插入图片描述

    2.3 构造函数继承

    function Parent(){
      this.name = 'parent1';
    }
    
    Parent.prototype.getName = function () {
      return this.name;
    }
    
    function Child(){
      Parent.call(this);
      this.type = 'child'
    }
    
    let child = new Child();
    console.log(child);  // 没问题
    console.log(child.__proto__);  // 原型上并没有getName方法
    console.log(child.getName());  // 会报错
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述
    可以看到,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法

    相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法

    2.4 组合继承

    function Parent3 () {
      this.name = 'parent3';
      this.play = [1, 2, 3];
    }
    
    Parent3.prototype.getName = function () {
      return this.name;
    }
    function Child3() {
      // 第二次调用 Parent3()
      Parent3.call(this);
      this.type = 'child3';
    }
    
    // 第一次调用 Parent3()
    Child3.prototype = new Parent3();
    // 手动挂上构造器,指向自己的构造函数
    Child3.prototype.constructor = Child3;
    var s3 = new Child3();
    var s4 = new Child3();
    s3.play.push(4);
    console.log(s3.play, s4.play);  // 不互相影响
    console.log(s3.getName()); // 正常输出'parent3'
    console.log(s4.getName()); // 正常输出'parent3'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这种方式看起来就没什么问题,方式一和方式二的问题都解决了,但是从上面代码我们也可以看到Parent3 执行了两次,造成了多构造一次的性能开销

    2.5 寄生式继承

    寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法

    let parent5 = {
        name: "parent5",
        friends: ["p1", "p2", "p3"],
        getName: function() {
            return this.name;
        }
    };
    
    function clone(original) {
        let clone = Object.create(original);
        clone.getFriends = function() {
            return this.friends;
        };
        return clone;
    }
    
    let person5 = clone(parent5);
    
    console.log(person5.getName()); // parent5
    console.log(person5.getFriends()); // ["p1", "p2", "p3"]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    其优缺点也很明显,跟上面讲的原型式继承一样

    2.6 寄生组合式继承

    寄生组合式继承,借助解决普通对象的继承问题的Object.create 方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式

    function clone (parent, child) {
        // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
        child.prototype = Object.create(parent.prototype);
        child.prototype.constructor = child;
    }
    
    function Parent6() {
        this.name = 'parent6';
        this.play = [1, 2, 3];
    }
    Parent6.prototype.getName = function () {
        return this.name;
    }
    function Child6() {
        Parent6.call(this);
        this.friends = 'child5';
    }
    
    clone(Parent6, Child6);
    
    Child6.prototype.getFriends = function () {
        return this.friends;
    }
    
    let person6 = new Child6();
    console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
    console.log(person6.getName()); // parent6
    console.log(person6.getFriends()); // child5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    可以看到 person6 打印出来的结果,属性都得到了继承,方法也没问题

    2.7 extends 方法

    文章一开头,我们是使用ES6 中的extends关键字直接实现 JavaScript的继承

    class Person {
      constructor(name) {
        this.name = name
      }
      // 原型方法
      // 即 Person.prototype.getName = function() { }
      // 下面可以简写为 getName() {...}
      getName = function () {
        console.log('Person:', this.name)
      }
    }
    class Gamer extends Person {
      constructor(name, age) {
        // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
        super(name)
        this.age = age
      }
    }
    const asuna = new Gamer('Asuna', 20)
    asuna.getName() // 成功访问到父类的方法
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    利用babel工具进行转换,我们会发现extends实际采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式

  • 相关阅读:
    JSR303和拦截器
    [vue3] Vue-Router(路由)使用
    铁死亡细胞实验相关抑制剂、激动剂
    2062. 统计字符串中的元音子字符串-c语言解法
    【c语言】指针和数组笔试题
    通讯录的实现(详解)
    Greenplum实用工具-gpfdist
    Spring事务@Transactional 注解下,事务失效的七种场景
    java基础之泛型
    Javascript 代码规范
  • 原文地址:https://blog.csdn.net/wu_2004/article/details/133084170