javascript六种继承方式

400 阅读6分钟

在了解js继承时,首先我们先来说明一下几个概念

  • 原型是什么?
    一个对象,名为prototype为原型对象

  • 原型的作用?
    共享方法或属性

  • 原型对象的custructor属性指向谁?作用是什么?
    指向该原型的构造函数,在改变原型对象的引用时,我们需要手动调用constructor让他指向原来的构造函数

  • 每个对象中都有一个内部指针__proto__,它指向原型对象

  • 原型链成员的查找规则?
    当前实例对象--->构造函数的原型--->Object的原型

es6的继承方式

  • 通过extends
  • 子类如果想要调用父类中的方法可以通过super.方法名()来调用
        class Person{
            constructor(name,age,sex){
                this.name = name,
                this.age = age,
                this.sex = sex;
            }
            show(){
                return `我叫${this.name},今年${this.age},性别${this.sex}`;
            }
        }

        // 继承
        class smallPerson extends Person{
            constructor(name,age,sex,habbit){
                super(name,age,sex);
                this.habbit = habbit;
                this.name = name;
                this.age = age;
                this.sex = sex;
            }
            showme(){
                return super.show()+`爱好是${this.habbit}`;
            }
            show(){
                console.log(`我叫${this.name},今年${this.age},性别${this.sex},爱好是${this.habbit}`);

            }
        }
        
        let p1 = new smallPerson('zh',20,'nan','study');
        p1.show();
        console.log(p1.showme());
        let p = new Person('zh',20,'nan');
        console.log(p.show());

原型链继承

    function Parent(money) {
      this.money = money
    }
    Parent.prototype.showMoney = function() {
      console.log(this.money)
    }

    function Son() {

    }
    Son.prototype = new Parent(10000000)
    Son.prototype.showMoney = function() {
      console.log(10000000000)
    }
    // Son.prototype.constructor = Son 
    let son = new Son()
    son.showMoney()
    // 这里的constructor指向的是Parent,因为Son的原型指向了另一个对象,
    // 所以内部的constructor属性,指向另一个对象的构造函数
    console.log(son.constructor)//Parent

上面的例子,父亲有一千万,但是可以被儿子继承,但是儿子重新赚到了更多的钱而不会影响父亲

  • 确定实例和原型的关系

    1. 通过instanceof
      这个是判断后者是否出现在该实例的原型链上
    2. 通过isPrototypeOf()方法
      判断调用该方法的对象是否出现在出入的实例对象的原型链上
  • 如果想要给继承者添加自己的方法,一定要将代码写在替换原型语句的后面,且不能用对象字面量的形式来创建自己的方法,这样会重写原型链

  • 原型链继承出现的问题

    1. 对于引用类型的值,当子类的一个实例改变引用类型的值时,通过该子类创建的实例也会改变,但是父类的不会改变。因为父类创建的对象和子类通过原型链继承的不是同一个对象。但是给对象添加新的属性,只会影响该对象自己,不会影响任何其他对象。
    function Parent() {
      this.name = 'zh'
      this.friends = ['111', '222']
    }
    Parent.prototype.showMoney = function() {
      console.log(this.money)
    }
    
    function Son() {
    
    }
    Son.prototype = new Parent()
    
    let son1 = new Son()
    let son2 = new Son()
    let p = new Parent()
    son1.friends.push('333')
    son1.name = "son1"
    console.log(son1.name, son1.friends)
    console.log(son2.name, son2.friends)
    console.log(p.friends) //['111', '222']
    

image.png 2. 不能向父类的构造函数中传参 3.打印对象的时候继承的属性是看不到的。

借用构造函数(经典继承/伪造对象)

  • 在子类的构造函数中调用父类的构造函数,利用(call(),apply()
  • 问题:虽然解决了共享引用类型的问题,但是子类无法获取父类定义的方法。

组合继承(伪经典继承)

最常用的继承模式

  • 使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承
    function Parent(money) {
      this.money = money
      this.friends = ['111', '222']
    }
    Parent.prototype.showMoney = function() {
      console.log(this.money)
    }

    function Son(money, age) {
      Parent.call(this,money)
      this.age = age
    }
    Son.prototype = new Parent()
    
    Son.prototype.showAge = function() {
      console.log(this.age)
    }

    let son1 = new Son(1000000,20)
    son1.friends.push('333')
    son1.showAge()//20
    son1.showMoney()//1000000
    console.log(son1.friends)//["111", "222", "333"]
    
    let son2 = new Son(1000000000, 39)
    son2.showMoney()//1000000000
    son2.showAge()//39
    console.log(son2.friends)//["111", "222"]

  • 也可以通过instanceof, isPrototypeOf()来识别基于组合继承创建的对象
  • 该方式出现的问题:
    1. 父类构造函数至少执行两次
    2. 通过继承父类方法使用的构造函数他会创建多余的属性,这些属性没有存在的必要。

image.png

原型式继承

    //原型式继承
    function object(obj) {
      // 创建一个临时构造函数
      function Foo() {}
      // 将传入的对象,作为构造函数的原型
      Foo.prototype = obj
      // 返回构造函数的实例
      return new Foo()
    }

    let obj = {
      name: 'zh',
      friends: ['hy','jcl','zxh','hcy']
    }

    let foo = object(obj)
    let foo1 = object(obj)
    foo.friends.push('ze')
    foo1.name = 'hy'
    console.log(foo.friends)//["hy", "jcl", "zxh", "hcy", "ze"]
    console.log(foo.name)//zh
    console.log(foo1.friends)//["hy", "jcl", "zxh", "hcy", "ze"]
    console.log(foo1.name)//hy
  • 该继承的本质是对传入的对象执行一次浅复制
  • 以一个对象为基础,传入函数中,返回一个实例,然后再根据具体需求对得到的对象加以修饰
  • 类似Object.create()方法
  • 或者通过这样
    function createObject(o) {
        const newObj = {}
        Object.setPrototypeOf(newObj, o)
        return newObj
    }

寄生式继承

  • 思路与寄生构造函数和工厂函数类似,即创建一个仅用于封装继承过程的函数,内部对对象做一些增强。
    function createObject(obj) {
      let o = Object.create(obj)
      o.showName = function() {
        alert(o.name)
      }
      return o
    }

    let obj = {
      name: 'zh'
    }

    let o = createObject(obj)
    o.showName()
  • 问题:由于做不到函数复用而降低效率

寄生组合式继承

    function Parent(money) {
      this.money = money
      this.friends = ['111', '222']
    }
    Parent.prototype.showMoney = function() {
      console.log(this.money)
    }

    function Son(money, age) {
      Parent.call(this,money)
      this.age = age
    }
    // 定义寄生组合式模型函数
    function createObject(subType, superType) {
      subType.prototype = Objec.create(superType.prototype)
      // 因为constructor是不可枚举属性
      Object.defineProperty(subType.prototype, "constructor", {
        enumerable: false,
        configurable: true,
        writable: true,
        value: subType
      })
    }
    //调用模型
    createObject(Son, Parent)

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

    let son1 = new Son(1000000,20)
    son1.friends.push('333')
    son1.showAge()
    son1.showMoney()
    console.log(son1.friends)
    
    let son2 = new Son(1000000000, 39)
    son2.showMoney()
    son2.showAge()
    console.log(son2.friends)
  • 不必为了指定子类型的原型而调用父类的构造函数,我们只是需要父类的原型的一个副本而已。

总结

  • 原型链继承

    缺点:

    1. 引用类型的属性被所有实例共享
    2. 在创建 子类 的实例时,不能向 父类 传参
    3. 打印对象时,原型上的属性不能显示
  • 借用构造函数(经典继承)

    优点:

    1. 避免了引用类型的属性被所有实例共享,因为每次创建对象,都会给该对象分配属性。
    2. 可以在 子类 中向 父类 传参 缺点:
    3. 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
  • 组合继承

    优点:

    1. 融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
  • 原型式继承

    缺点:

    1. 包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
  • 寄生式继承

    缺点:

    1. 跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
  • 寄生组合式继承

    优点:

    1. 这种方式的高效率体现它只调用了一次 父类 构造函数,并且因此避免了在 父类的prototype 上面创建不必要的、多余的属性。
    2. 与此同时,原型链还能保持不变。
    3. 因此,还能够正常使用 instanceof 和 isPrototypeOf。

开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式

下面我们来分析一下组合式继承的内存分析

    // 父类: 公共属性和方法
    function Person(name, age, friends) {
      this.name = name
      this.age = age
      this.friends = friends
    }

    Person.prototype.eating = function() {
      console.log(this.name + " eating~")
    }

    // 子类: 特有属性和方法
    function Student(name, age, friends, sno) {
      Person.call(this, name, age, friends)
      this.sno = sno
    }

    var p = new Person()
    Student.prototype = p

    Student.prototype.studying = function() {
      console.log(this.name + " studying~")
    }
    
    var stu1 = new Student("why", 18, ["lilei"], 111)
    var stu2 = new Student("kobe", 30, ["james"], 112)

image.png

下面我们来分析一下原型链继承的内存分析

    // 父类: 公共属性和方法
    function Person() {
      this.name = "why"
      this.friends = []
    }

    Person.prototype.eating = function() {
      console.log(this.name + " eating~")
    }

    // 子类: 特有属性和方法
    function Student() {
      this.sno = 111
    }

    var p = new Person()
    Student.prototype = p

    Student.prototype.studying = function() {
      console.log(this.name + " studying~")
    }


    // name/sno
    var stu1 = new Student()
    var stu2 = new Student()
    stu1.name = "kobe"

image.png