《JavaScript---深入理解面向对象(2)》

161 阅读6分钟

深入理解面向对象-继承

前言

面向对象有三大特性:封装、继承、多态

  • 封装:上一文深入理解面向对象将属性和方法封装到一个函数(类)中,可以称之为封装的过程;
  • 继承:继承面向对象中非常重要的,不仅可以减少重复代码的数量,也是多态的前提(纯面向对象中)
  • 多态:不同的对象在执行任务时的不同表现 在真正实现继承之前,我们先来理解一个非常重要的概念:原型链

JavaScript的原型链

我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去他的原型上面获取:

image.png 那么什么地方时原型的尽头的?比如我们第三个对象是否也有原型__proto__的属性呢?

image.png 我们会发现它打印的是 [Object:null prototype]{} ,事实上这个原型就是我们最顶层的原型了,从Object直接创建出来的对象的原型都是 [Object:null prototype]{} 。 那么我们可能会问:[Object:null prototype]{} 原型有什么特殊?

  • 特殊一:该对象有原型属性,但是他的原型属性已经指向null,也就是顶层原型
  • 特殊二:该对象上有很多默认的方法和属性

image.png 原型链关系内存图

image.png 从上面给的Object的原型我们可以得出一个结论:原型链最顶层的原型对象就是Object的原型对象null

image.png

如果我们现在需要实现继承,那么就可以利用原型链来是实现了

通过原型链实现继承

 // 1、定义父类构造函数
  function Person (){
      this.name = 'why'
      this.friend = []
  }
  
  //2、父类原型上添加方法
  Person.prototype.running = function(){
      console.log(this.name + "running")
  }    
  
  //3、定义子类构造函数
  function Student(){
      this.ano = "111"
   }
   
   //4、通过原型链实现继承:创建父类实例,并且作为子类的protptype原型对象
   let p = new Person()
   Student.prototype = p
   
   //5、在子类原型上添加新的内容
   Student.prototpye.studying = function(){
       console.log(this.name + "studying")
    }   

目前student的原型是p对象,而p对象的原型是Person默认的原型,里面包含running等函数,注:步骤4和步骤5不可以调整顺序,否则会有问题

image.png

原型链继承的弊端

使用原型链继承的弊端:某些属性其实是保存在p对象上的;

  • 第一:我们通过直接打印对象看不到这个属性的;例如student.name
  • 第二:这个属性会被多个对象共享,如果这个对象是一个引用类型,会造成问题
    let stu1 = new Student()
    let stu2 = new Student()
    //直接修改stu1上的属性,是给本对象添加属性
    stu1.name = "kobe" 
    console.log(stu1.name)  // "kobe"
    console.log(stu2.name)  // "why"
    //获取引用, 修改引用中的值, 会相互影响
    stu1.friends.push("kobe")
    console.log(stu1.friends)  // ["kobe"]
    console.log(stu2.friends)  // ["kobe"]
    
  • 第三:不能给Person传递参数,因为这个对象是一次性创建的

借用构造函数继承

为了解决原型链中存在的问题,开发人员提供了一种新的技术:constructor stealing(借用构造函数)。 借用构造函数的做法很简单:在子类构造函数内部调用父类构造函数

  • 函数可以在任意时刻被调用
  • 通过apply()和call()方法也可以在新创建的对象上执行构造函数
   // 父类: 公共属性和方法
    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 = stu
      // this.name = name
      // this.age = age
      // this.friends = friends
      this.sno = 111
    }

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

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

image.png

组合借用继承的问题

  • 组合借用构造函数继承最大的问题就是无论在什么情况下,都需要调用两次父类构造函数
    • 一次是在创建子类原型的时候
    • 另一侧是在子类构造函数内部(也就是每次创建子类实例的时候)
  • 另外,如果仔细按照流程走每个步骤,你就会发现,每个子类实例事实上会有两份父类的属性
    • 一份在当前的实例自己里面(也就是Person本身的),另一份在子类对应的原型对象中(也就是preson.__proto__里面)
    • 当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的;

原型式继承函数

  • 原型式继承的渊源
    • 这种模式要从道格拉斯·可罗克福德在2006年写的一篇文章说起:Prototypal Inheritance in JavaScript(在JS中使用原型式继承)
    • 在这篇文章中,它介绍了一种继承方式,而且这种继承方式不是通过构造函数来实现的
    • 为了理解这种方式,我们先再次回顾一下JavaScript想实现继承的目的:重复利用另外一个对象的属性和方法
  • 最终的目的:student对象的原型指向了person对象

image.png

寄生式继承函数

  • 寄生式继承是与原型链继承紧密相关的一种思想
  • 寄生式继承的思路是结合原型链继承和工厂模式的一种方式
  • 即创建一个封装继承的函数,该函数内部以某种方法来增强对象,最后返回这个对象

image.png

寄生组合式继承

我们来回顾一下之前提出的比较理想的组合继承,组合继承是比较理想的继承方式,但是存在两个问题

  1. 构造函数被调用两次:一次在创建子类原型对象的时候,另一次是在创建子类实例对象的时候
  2. 父类中的属性会有两份:一份在原型对象上,另一份在子类型中 事实上,我们可以利用寄生式继承将这两个问题解决掉
  • 首先明确一点:当在子类构造函数中调用父类.call(this,参数)这个函数的时候,就会在将父类的属性赋值一份到子类中,所以父类本身里面的内容,我们不再需要
  • 这个时候,我们还需要获取到一份父类的原型对象中的属性和方法 能不能直接让子类型的原型对象=夫类型的原型对象呢?
  • 首先明确表示我们不应该这么做,因为这么做就意味着修改了子类原型对象的某个引用类型的时候,父类原生对象的引用也会改变
  • 所以我们考虑创建一个中间量,使用前面的寄生式思想就可以了
   //定义object函数
   function object(o){
       function F(){}
       F.prototype = o
       return new F
  }
  //定义寄生式核心函数
  function inheritProtptype(subType,superType){
      let newObj = object(superType.prototpye)
      subType.prototype = newObj
      subType.prototype.constructor = subType
  }    
  
   function inheritProtptype(Student,Person)

小结

综上所述,ES5继承的最优解如下代码所示:

  //1、定义父类的属性
  function Person (name,age,friends){
      this.name =name
      this.age = age
      this.friends = friends
  }
  // 2、定义父类的方法
  Person.prototype.running = function(){
      console.log(this.name + " is running")
  }
  
  
  //3、定义子类
  //3.1子类属性继承
  function Student(name,age,friends,height,score){
      //组合式继承属性
      Person.call(this,name,age,friends)
      this.height = height
      this.score = score
  }
  //3.2子类方法继承
  //第一种:利用上面封装的寄生式函数
  function inheritProtptype(Student,Person)
  
  //第二种:利用Object.create()方法和Object.defineProperty
  Student.prototype = Object.create(Person.prototype)
  Object.defineProperty(Student.prototype,"constructor",{
      configurable:true,
      enumable:false,
      writable:true,
      value:Student
   })
   Student.prototype.studying = function() {
      console.log("studying~")
   }
  
  var stu = new Student("why", 18, ["kobe"], 111, 100)
  console.log(stu) 
  // Student { name: 'why', age: 18, friends: [ 'kobe' ],height:1.88, score: 100}
  stu.studying()   // studying~
  stu.running()    //why is running