Javascript中的继承

119 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

Javascript中的继承

总览五种继承方式

  // 1、原型链继承
  function Animal() {
    this.colors = ['black', 'red']
  }
  function Dog() {}
  Dog.prototype = new Animal() // 原型链继承的特点
  
  // 2、借用构造函数继承
  function Animal(colors) {
    this.colors = colors
    this.getColors = function () {
      return this.colors
    }
  }
  function Dog(colors) {
    Animal.call(this, colors) // 借用构造函数继承的特点
  }
  
  // 3、组合继承
    function Animal(name) {
    this.name = name
    this.colors = ["black", "white"]
  }
  Animal.prototype.getName = function () {
    return this.name
  }
  function Dog(name, age) {
    Animal.call(this, name) // 借用构造函数继承的特点
    this.age = age
  }
  Dog.prototype = new Animal() // 原型链继承的特点
  Dog.prototype.constructor = Dog
  
  // 4、寄生式组合继承
  function Animal(name) {
    this.name = name
    this.colors = ["black", "white"]
  }
  Animal.prototype.getName = function () {
    return this.name
  }

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

  Dog.prototype = Object.create(Animal.prototype)
  Dog.prototype.constructor = Dog
  
  // 5、ES6实现继承
    class Animal {
    constructor(name) {
      this.name = name
    }
    // 方法是在构造器外面的
    getName() {
      return this.name
    }
  }

  // extends关键字实现继承
  class Dog extends Animal {
    constructor(name, age) {
      super(name)
      this.age = age
    }
  }
  

1、原型链继承

  function Animal() {
    this.colors = ['black', 'red']
  }
  function Dog() {}
  Dog.prototype = new Animal() // 原型链继承的特点

上下区别不大,下面只是表明在Animal原型上面定义的方法,也是同样可以被继承的,可以只看上面的

  function Animal() {
    this.colors = ['black', 'red']
  }
  // 这部分和下面的分析关系不大,但是原型链继承时,该方法也是能够被继承的
  Animal.prototype.getColor = function () { 
    return this.colors
  }
  function Dog() {}
  Dog.prototype = new Animal() // 原型链继承的特点

原型链继承的两个缺点

缺点1:子类在实例化的时候不能给父类构造函数传参数,因为子类构造函数里面没有调用父类构造函数

  function Animal() {
    this.colors = ['black', 'red']
  }
  Animal.prototype.getColor = function () {
    return this.colors
  }
  function Dog() {
    // 缺点1:
    // 子类在实例化的时候不能给父类构造函数传参数
    // 因为子类构造函数里面没有调用父类构造函数
  }
  Dog.prototype = new Animal()

缺点2:原型中包含的属性将被所有实例共享,注意是原型中包含的属性,并不新建实例上的属性,而是新建实例的原型上的属性

新建Dog的实例dog1,并且打印dog1.colors

let dog1 = new Dog()
console.log(dog1.colors) // ['black', 'red']

dog1的colors属性,明明没有这个属性,为什么能打印出来呢? 因为在dog1上找不到就去dog1的原型上面找这个属性

let dog1 = new Dog()
console.log(dog1.colors) // ['black', 'red']
console.log(dog1.__proto__.colors) // ['black', 'red']

往dog1.colors中添加一个值,其实这里也是给dog1原型上的colors添加一个值

dog1.colors.push('blue')  
// 等价于
dog1.__proto__.colors.push('blue')

再次打印dog1.colors

console.log(dog1.colors) // ['black', 'red', 'blue']
console.log(dog1.__proto__.colors) // ['black', 'red', 'blue']

新建Dog的实例dog2,并且打印dog2.colors

let dog2 = new Dog()
console.log(dog2.colors) //['black', 'red','blue']
console.log(dog2.__proto__.colors) //['black', 'red','blue']

为什么和dog1刚开始打印dog1.colors时显示的不一样呢,而是和往dog1.colors里面push数据后的一样?因为dog1和dog2共用一个原型,当dog2上找不到colors这个属性的时候,就去原型上面找,因为共用原型,所以打印的结果是原型上面的colors。

2、借用构造函数继承

  function Animal(colors) {
    this.colors = colors
    this.getColors = function () {
      return this.colors
    }
  }

  function Dog(colors) {
    Animal.call(this, colors) // 借用构造函数继承的特点
  }
  
  // 无法实现构造函数的复用,每个子类都有父类实例函数的副本,影响性能,代码会臃肿
  function DogCopy(colors) {
    // 父类实例函数的副本
    Animal.call(this, colors) 
  }
  
  let dog1 = new Dog(["blue", "red"])

优点:解决了原型链继承的两个缺点

借用构造函数继承的两个缺点

缺点一,这种方法其实就没有用到父类原型,没有涉及到Animal.prototype的代码,和第一种继承法(原型链继承)形成鲜明的对比,只是使用了call()或者apply()方法,调用父类的构造函数,所以不能继承父类原型属性和方法,只能继承父类的实例属性和方法(即父类构造函数中的东西)。

缺点二,无法实现构造函数的复用,每个子类都有父类实例函数的副本,影响性能,代码会臃肿。

// 新建实例dog1
let dog1 = new Dog(["blue", "red"])
console.log(dog1.colors) // ['blue', 'red']
console.log(dog1.__proto__.colors) //undefined

// dog1.colors中push一个值
dog1.colors.push("black")
console.log(dog1.colors) // ['blue', 'red', 'black']
console.log(dog1.__proto__.colors) //undefined

// 新建实例dog2
let dog2 = new Dog(["yellow", "green"])
console.log(dog2.colors) // ['yellow', 'green']
console.log(dog2.__proto__.colors) //undefined

3、组合继承

组合继承就是结合原型链继承借用构造函数继承的继承方式,将两者的优点集中。

  function Animal(name) {
    this.name = name
    this.colors = ["black", "white"]
  }
  Animal.prototype.getName = function () {
    return this.name
  }
  function Dog(name, age) {
    Animal.call(this, name) // 借用构造函数继承的特点
    this.age = age
  }
  Dog.prototype = new Animal() // 原型链继承的特点
  Dog.prototype.constructor = Dog // 这一步有什么作用呢???

实现思路

  1. 使用原型链继承,继承父类原型上的属性和方法
  2. 通过借用构造函数继承,继承父类实例上的属性和方法

这样既可以把方法定义在原型上实现重用,又可以让每个实例都有自己的属性

let dog1 = new Dog("小花", 2)
dog1.colors.push("brown")
console.log(dog1) //  {name: '小花', colors:['black', 'white', 'brown'], age: 2}

let dog2 = new Dog("小白", 1)
console.log(dog2) // {name: '小白', colors:['black', 'white'], age: 1}

组合继承的缺点:调用了两次父类构造函数

  1. 第一次是在new Animal()
  2. 第二次是在Animal.call()
 function Animal(name) {
    this.name = name
    this.colors = ["black", "white"]
  }
  Animal.prototype.getName = function () {
    return this.name
  }
  function Dog(name, age) {
    Animal.call(this, name) // 第二次
    this.age = age
  }
  Dog.prototype = new Animal() // 第一次
  Dog.prototype.constructor = Dog

思考:这一步有什么作用呢?

Dog.prototype.constructor = Dog之前的原型链继承和借用构造函数继承都没有出现这一步,为什么这边会出现这么一步?

目的就是在原型链中加上标红的这一步,使得原型链更加完整。

Snipaste_2022-07-15_23-41-45.png

4、寄生式组合继承(最成熟的)

  function Animal(name) {
    this.name = name
    this.colors = ["black", "white"]
  }
  Animal.prototype.getName = function () {
    return this.name
  }

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

  //   Dog.prototype = new Animal();
  //   Dog.prototype.constructor = Dog;
  //   将这两行注释了换成下面的

  function F() {}
  F.prototype = Animal.prototype
  let f = new F()
  f.constructor = Dog
  Dog.prototype = f

这三步,返回了一个由父类构造函数实例化的一个对象f

function F() {}
F.prototype = Animal.prototype
let f = new F()

这两步,将父类构造函数实例化的对象f与Dog的原型相互绑定,从而实现Dog继承Animal

f.constructor = Dog
Dog.prototype = f

简单封装

  function Animal(name) {
    this.name = name
    this.colors = ["black", "white"]
  }
  Animal.prototype.getName = function () {
    return this.name
  }

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

  function object(o) {
    function F() {}
    F.prototype = o
    let f = new F()
    return f
  }
  function inheritPrototype(son, father) {
    let prototype = object(father.prototype)
    prototype.constructor = son
    son.prototype = prototype
  }
  inheritPrototype(Dog, Animal)

基于组合继承改写

基于组合继承的代码改成最简单的寄生式组合继承

  function Animal(name) {
    this.name = name
    this.colors = ["black", "white"]
  }
  Animal.prototype.getName = function () {
    return this.name
  }

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

  Dog.prototype = Object.create(Animal.prototype)
  Dog.prototype.constructor = Dog

5、ES6实现继承

ES中关键字extends实现继承

  class Animal {
    constructor(name) {
      this.name = name
    }
    // 方法是在构造器外面的
    getName() {
      return this.name
    }
  }

  // extends关键字实现继承
  class Dog extends Animal {
    constructor(name, age) {
      super(name)
      this.age = age
    }
  }

思考:babel中将class转换时是转成es5的哪种继承?