前端JS如何实现继承

513 阅读7分钟

前端JS如何实现继承

构造函数的应用

当多个构造函数需要使用一些共同的方法或者属性的时候 我们需要把这些共同的东西拿出来, 单独书写一个构造函数 让其他的构造函数去继承自这个公共的构造函数

概念

  • B 构造函数的实例能够使用 A 构造函数的属性和方法
  • 我们管 B 构造函数叫做 A 构造函数的子类
  • 我们关 A 构造函数叫做 B 构造函数的父类

目的 :B 构造函数能够使用 A 构造函数的属性和方法

继承前的准备:准备一个父类

function Person(name, age) {
      this.name = name
      this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }

那么此时我们new出来一个Person实例应该长这样:

Person 的实例 = {
     name: 'Jack',
     age: 18,
     __proto__: { // Person.prototype
        constructor: Person,
        sayHi: function () { },
        __proto__: Object.prototype
    }
}

一、原型继承

利用改变 原型链 的方式来达到继承效果   直接把父类的实例当作子类的 prototype

父类在前面已经准备好了,我们现在定义一个子类

// 子类
    function Student(gender) {
      this.gender = gender
    }

需求: 子类继承父类的name 、age 、sayHi

核心代码:

子类.prototype = new 父类

需求实现:Student.prototype = new Person('Jack', 18)

// 子类
function Student(gender) {
  this.gender = gender
}

// 直接把父类的实例当作字类的原型对象
Student.prototype = new Person('Jack', 18)

此时Student类的原型应该长这样

Student.prototype = {
    name: 'Jack',
    age: 18,
    __proto__: { // Person.prototype
      constructor: Person,
      sayHi: function () { },
      __proto__: Object.prototype
    }
}

而当我们const s = new Student('男')时,s应该长这样

s = {
    gender: '男',
    __proto__: { // Student.prototype 也是 Person 的实例
      name: 'Jack',
      age: 18,
      __proto__: { // Person.prototype
        constructor: Person,
        sayHi: function () { },
        __proto__: Object.prototype
      }
    }
}

此时的原型链示意图:

1666693539759.jpg

原型继承的优缺点

  • 优点:
    1. 构造函数体内和原型上的都可以继承
    2. 写法方便简洁,容易理解。
  • 缺点:
    1. 一个构造函数的内容, 在两个位置传递参数
    2. 继承来的属性不再子类实例的身上

二、借用继承

借用构造函数继承   (借用继承 / call继承)

原理:通过改变 父类 构造函数的 this 指向来达到继承效果

核心代码:在子类构造函数体内,   父类.call(字类的实例)

我们要了解借用继承之前必须要知道的知识:

构造函数的执行:

1. 是一个普通函数, 可以当作函数直接调用
2. 当作普通函数执行的时候, this 指向谁, 就向谁身上添加内容
3. call 方法可以改变函数的 this 指向

代码实现

    function Student(gender, name, age) {
      this.gender = gender
      // Person('Jack', 18)
      // 使用 call 方法改变一下 Person 函数内部的 this 指向
      // 改变成指向谁, Person 就会向谁的身上添加一个 name 一个 age
      // Person.call('Jack', 18)
      // 这个位置的 this 指向 Student 的实例, 因为 new Student
      // Person 函数内部的 this 指向 Student 的实例 this === s
      Person.call(this, name, age)
      // 这个函数执行完毕以后, 会像 Student 的实例身上添加一个 name 一个 age
    }

借用继承的优缺点

  • 优点:
    1. 继承来的属性是在自己身上
    2. 我们一个实例化过程在一个位置传递参数
  • 缺点:
    1. 只能继承父类构造函数体内的内容
    2. 父类原型上的内容不能继承
    3. 我们只是把父类当成普通函数调用了一下而已,方法是继承不到的

三、最早的组合继承

原理:把 原型继承借用构造函数继承 合并在一起使用

代码实现

    function Student(gender, name, age) {
      this.gender = gender

      // 借用继承, 目的: 把属性继承在自己身上
      Person.call(this, name, age)
    }

    // 原型继承, 目的: 继承父类原型上的方法
    Student.prototype = new Person()

    // 书写属于 Student 自己的方法
    Student.prototype.study = function () { console.log('study') }

    // 使用 Student 创建实例
    const s = new Student('男', 'Jack', 18)

组合继承的优缺点

  • 优点:
    1. 父类构造函数体内和原型上的内容都能继承
    2. 继承下来的属性放在自己身上
    3. 在一个位置传递所有参数
  • 缺点:
    1. 当你给字类添加方法的时候, 实际上是添加在了父类的实例身上

四、拷贝继承

原理:利用 for in 循环的特点, 来继承所有的内容

步骤:

  1. 先实例化一个父类的实例
  2. 使用 for in 循环来遍历这个实例对象 => 因为 for in 循环不光遍历对象自己, 还会遍历 __proto__
  3. 直接把父类实例身上的所有内容直接复制到字类的 prototype

代码实现

function Student(gender, name, age) {
  this.gender = gender

  // for in 继承
  const p = new Person(name, age)
  for (let key in p) {
    Student.prototype[key] = p[key]
  }
}

Student.prototype.study = function () { console.log('study') }

const s = new Student('男', 'Jack', 18)
console.log(s)

拷贝继承的优缺点

  • 优点:
    1. 父类的构造函数体内的和原型上的都可以继承
    2. constructor 能正常配套
    3. 添加自己的方法的时候, 确实是在自己的原型身上
    4. 一个地方传参
  • 缺点:
    1. for in 循环: for in 循环需要一直遍历到 Object.prototype
    2. 对性能的消耗比较大
    3. 不能继承 不可枚举 的属性
    4. 继承来的属性不在自己身上

五、寄生继承

1.寄生实例

关键代码:

const instance = new Person(name, age)

return instance

代码实现:

    function Student(name, age) {
      this.gender=  '男'

      // 寄生继承
      const instance = new Person(name, age)
      return instance
    }
    
    const s = new Student('Jack', 18)

    Student.prototype.study = function () {}
    console.log(s)

仔细研究上面的代码我们会发现一些端倪

我们可以看一下控制台的打印结果:

3.png

  1. s 确实是 new Student 来的
  2. s 就是 Student 的实例, 但是真实的内容是 Person 的实例

最重要的是:study方法竟然也不在原型链上了,这就很难搞了,就有了寄生继承的另一种版本

2.寄生原型

不直接寄生实例, 寄生原型

代码实现:

    function Student(gender) {
      this.gender = gender
    }

    // 寄生原型
    Student.prototype = Person.prototype

    // 该自己的
    Student.prototype.stduy = function () {}

    const s = new Student('男')
    console.log(s)

我们可以看一下控制台的打印结果:

image.png

表面上看着很完美,属性和方法都完整继承下来了,而且自己写的方法也没有任何影响,但是它却有着致命的缺陷,嘿嘿,Student.prototype = Person.prototype 这两个东西指向的是一个地址,那么你往Student.prototype上面挂东西的时候,直接操作了Person.prototype,别的实例也会受到影响了

寄生继承的优缺点

  • 优点:
    1. 原型和构造函数体内的都能继承下来
    2. 寄生原型的话, 自己的属性和方法依旧可以添加和使用
  • 缺点:
    1. 寄生实例的时候, 没有自己的任何内容
    2. 寄生原型的时候, 一旦修改原型上的任何属性方法, 父类的实例也会有这些属性方法

本来写完了,但还是忍不住回来吐槽一下这个设计!我们都知道构造函数不应该写 return 的,因为当你 return 一个基本数据类型的时候,你写也是白写!而当你 return 一个复杂数据类型, 构造函数就没有意义了!当然啦,奈何人家确实也能完成继承,就这样吧

六、寄生式组合继承 (完美继承)

合并了 寄生继承 + 原型继承 + 独立第三方构造函数 + 借用继承

代码实现

    function Student(gender, name, age) {
      this.gender = gender
      // 借用继承: 继承来了父类的属性
      Person.call(this, name, age)
    }

    (function () {
      function Abc(name, age) {}
      // 让 第三方构造函数 来寄生 父类 的原型
      Abc.prototype = Person.prototype
      Student.prototype = new Abc()
    })()

看一下最终结果:

image.png

  • 继承来的和自带的属性在自己身上
  • 可以将父类的原型上也继承下来了
  • 添加自己的方法不影响其他
  • 完美

七、ES6的类继承

官方把 寄生式组合继承class关键字封装起来了

前面的理解之后这个就太水了,直接上代码吧

    class Student extends Person {
      constructor (gender, name, age) {
        super(name, age)
        this.gender = gender
      }
      study () {
        console.log('study')
      }
    }
  1. extends

class 子类类名 extends 父类 { }

  1. super()

在 constructor 里面书写一个 super()

super( name , age ) 等价于 Person.call ( this , name , age )

注意

  1. super 需要写在 constructor 里面
  2. 如果你要写自己的属性, 必须写在 super 后面
  3. ES6 的继承可以继承 ES5 的构造函数也可以继承 ES6 的类