In 2022, I am learning JS inheritance and class again

Prototype and inheritance are relatively important knowledge in JS. Learning and using them proficiently will help us a lot in our daily development and interviews. In previous articles, I have introduced the content related to prototypes in detail. Learning prototypes and prototype chains well is the basic prerequisite for mastering inheritance. If you are interested, you can check # 2022 I will learn JS prototypes and prototype chains again , so I won’t introduce too many prototypes here. . This article mainly introduces inheritance, including common inheritance methods in es5, inheritance of new classes in es6, and what the new operator does internally when creating an object.

Prototype chain inheritance:

Those who are familiar with prototypes and prototype chains are clear:

  • Every object will have a __proto__ property that points to the prototype object of the constructor that instantiates the object
  • When accessing a property or method of an object, the instance itself does not have it. First , the prototype object of the __proto__constructor will be found along the prototype chain through the object's properties.
  • If the prototype object of the constructor is not found, it will continue to access the __proto__properties of the prototype object to find it, until it finds the end of the prototype chain Object.prototype.__proto__, which is null.

According to the characteristics of the prototype mentioned above, if you assign the subclass constructor to the 原型对象one you want to inherit 父类构造函数的实例, the subclass instance will access the constructor when it has no attributes. At 原型对象this time, it 原型对象has been assigned to the parent class. Constructed 父类实例对象, if there are no properties, 父类实例对象the prototype object of the constructor will be accessed. This achieves prototype chain inheritance.

Code demo:

  function Father() {
    this.name = 'Jack'
    this.like = ['play', 'sleep']
  }
  Father.prototype.getName = function() {
    console.log(this.name)
  }

  function Son() {
    this.age = 18
  }
  Son.prototype = new Father() // 关键步骤,把Son的原型对象指向Father的实例对象

  let son = new Son()

  // 对象son本身自带的属性
  console.log(son.age)  

  // 对象son本身没有,通过son.__proto__访问Son.prototype,这时Son.prototype已经指向了new Father()创建出来的实例对象,找到name属性
  console.log(son.name) 

  // 对象son本身没有,通过son.__proto__访问Son.prototype也没有,继续访问Son.prototype.__proto__,因为Son.prototype等于new Father()创建出来的实例对象,所以Son.prototype.__proto__指向Father.prototype,找到getName方法。
  console.log(son.getName())  

  // 子类从父类继承的引用类型的属性,因为是堆内存中的同一个值,会互相影响
  let son1 = new Son()
  son1.like.push('eat')
  let son2 = new Son()
  console.log(son1.like) // ['play', 'sleep', 'eat']
  console.log(son2.like) // ['play', 'sleep', 'eat']
复制代码

shortcoming:

  1. In the constructor of the parent class, the attributes of the reference type will be shared, and when a subclass is modified, it will affect other subclasses.
  2. When instantiating a subclass, you cannot pass arguments to the parent class.

Borrowing constructor inheritance:

In the subclass constructor, the parent class constructor is called, and the properties on the parent class instance are inherited by changing the this pointer when the parent class constructor is executed.

Code demo:

  function Father(name) {
    this.name = name
    this.like = ['play', 'sleep']
  }
  Father.prototype.getName = function() {
    console.log(this.getName)
  }

  function Son(name, age) {
    Father.call(this, name) // 调用父类构造函数,改变this指向,传入需要的参数
    this.age = age
  }

  let son = new Son('Jack', 18) 

  // 对象son本身就具有age和name属性
  console.log(son.age) // 18
  console.log(son.name) // Jack

  // 访问不到方法,报错 ,没有继承父类原型上的属性
  console.log(son.getName())
  
  // 每次实例化对象,都会重新执行构造函数,引用类型的属性不会共享
  let son1 = new Son('Jack', 18)
  let son2 = new Son('Mary', 18)
  son1.like.push('eat')
  console.log(son1.like) // ['play', 'sleep', 'eat']
  console.log(son2.like) // ['play', 'sleep']
复制代码

Since each time a subclass instance is created, the constructor of the parent class will be executed once, so the properties inherited by the subclass from the parent class are unique, which solves the problem of prototype chain inheritance, when the subclass inherits the properties of the reference type in the parent class , the problem of mutual influence.

shortcoming:

  1. 无法继承父类原型链的属性 (执行son.getName()时报错)

组合继承:

把原型链继承和借用构造函数继承组合在一起,就是组合继承的实现方式。

代码演示:

  function Father(name) {
    this.name = name
    this.like = ['play', 'sleep']
  }
  Father.prototype.getName = function() {
    console.log(this.name)
  }

  function Son(name, age) {
    Father.call(this, name)  // 调用Father构造函数,继承Father实例对象的属性。实例子类时,支持传参
    this.age = age
  }
  Son.prototype = new Father('Jack') // 调用Father构造函数,继承原型对象上的属性

  let son = new Son('Jack', 18)
  console.log(son.age)
  console.log(son.name)
  console.log(son.getName())
复制代码

组合继承解决了原型链继承中,子类实例的对象修改父类的引用类型的值时,影响其他子类实例。同时解决了借用构造函数继承中,无法继承父类原型对象的属性问题。

但是因为执行了两次父类构造函数,部分属性会同时存在在对象son上和son.__proto__上,控制台打印son:

wecom-temp-48af918bb287958dd20a7e728d8644c0.png

缺点:

  1. 调用两次父类构造函数,造成了不必要的开销,一些属性在子类实例上和原型上重复。

原型式继承:

这种继承方式,还是基于原型、原型链的知识。借助原型,可以基于已有的对象创建一个新的对象:

  function objectCreate(o) {
    function F() {}
    F.prototype = o
    let obj = new F()
    return obj
  }
复制代码

在objectCreate函数内部,有一个构造函数F,把传入的对象作为F的原型,在实例化一个新对象,最终返回。这样就创建了一个以传入的对象作为原型的新对象。实际上,objectCreate函数对传入的对象执行了一次浅复制,看下面的例子:

let person = {
  name: 'Tom',
  like: ['play', 'sleep']
}

let person1 = objectCreate(person)
person1.name = 'Jerry'
person1.like.push('eat')

let person2 = objectCreate(person)
person2.name = 'Jack'
person2.like.push('cry')

console.log(person.like)  // ['play', 'sleep', 'eat', 'cry']
复制代码

es6中的Object.create方法规范原型式继承。这个方法接受两个参数:

  1. 作为新对象原型的对象。
  2. 作为新对象定义额外属性的对象。

当只传入第一个参数时,作用和objectCreate函数一样

let person = {
  name: 'Tom',
  like: ['play', 'sleep']
}

let person1 = Object.create(person)
person1.name = 'Jerry'
person1.like.push('eat')

let person2 = Object.create(person)
person2.name = 'Jack'
person2.like.push('cry')

console.log(person.like)  // ['play', 'sleep', 'eat', 'cry']
复制代码

Object.create第二个参数和Object.defineProperties方法的第二个参数一样,每个属性的描述符。用这种方式定义的属性,会覆盖原型对象上的同名属性。

let person = {
  name: 'Tom',
  like: ['play', 'sleep']
}

let person1 = Object.create(person, {
  name: {
    value: 'Jerry'
  }
})
console.log(person1.name)  // Jerry
复制代码

寄生式继承:

寄生式继承于原型式继承类似,同样是利用一个对象作为新对象的原型,在以某种方式增强新对象。

  function objectCreate(o) {
    function F() {}
    F.prototype = o
    return new F()
  }
  function createAnother(o) {
    var obj = objectCreate(o) // 把o作为新对象的原型
    obj.sayHi = function() {  // 增强新对象
      console.log('Hi')
    }
    return obj
  }

  let person = {
    name: 'Tom',
    like: ['play', 'sleep']
  }
  let person1 = createAnother(person)
  person1.sayHi()
复制代码

使用createAnother创建一个对象,新对象不仅继承了person上面的属性,还有一个sayHi方法。

寄生组合式继承:

上面提到了组合继承解决了原型链继承和借用构造函数继承的一些缺点,是一种比较完善的继承方式。但是它也存在缺点,就是调用了两次父类构造函数,一次是在创建子类的时候,一次是在子类构造函数内部。
虽然做到了同时继承父类实例属性,和父类原型属性,但是由于调用了两次父类构造函数,做了没有必要的开销,同时在子类实例和原型上存在重复属性。

寄生组合式继承就可以解决这一问题:

  • 通过借用构造函数继承,来继承父类实例上的属性。
  • 子类不必在通过将原型指向父类的实例。直接使用寄生式继承,用父类构造函数的原型创建一个临时对象,在把子类的原型指向临时对象。
function objectCreate(o) {
  function F() {}
  F.prototype = o
  return new F()
}

function extendsPrototype(subType, superType) {
  // 用father的原型,创建一个临时对象, 等价于:Object.create(superType.prototype)
  let prototype = objectCreate(superType.prototype) 

  // 增强对象,把临时对象的constructor 指向子类构造函数
  prototype.constructor = subType  

  // 子类把原型对象指向 新对象
  subType.prototype = prototype 
}

function Father(name) {
  this.name = name
  this.like = ['play', 'sleep']
}

Father.prototype.getName = function() {
  console.log(this.name)
}

function Son(name, age) {
  Father.call(this, name)
  this.age = age
}

extendsPrototype(Son, Father)
Son.prototype.getAge = function() {
  console.log(this.age)
}

let son = new Son('Tom', 18)
复制代码

在extendsPrototype函数中,为了继承父类原型的属性,通过寄生式继承创建了一个临时对象,再让子类的原型指向这个临时对象,这样就继承了父类原型上的属性。为什么不直接把子类原型指向父类原型呢?这样同样可以让子类访问父类的原型属性,像这样:

function extendsPrototype(subType, superType) {
  subType.prototype = superType.prototype 
}
复制代码

通过寄生式继承创建一个临时对象来继承父类,主要目的是为了保证继承这一链条会正确的沿着原型链查找属性,并且能正常使用instanceof和isPrototypeOf()判断对象和对象之间的继承关系。

new操作符具体步骤:

我们通常使用new操作符来创建一个对象,他的原理就是通过上面说到继承方式来实现的。我们通过实现一个简易的new来理解一下:

function myNew(Con, ...args) {
  let obj = {}
  obj.__proto__ = Con.prototype // 等同于:Object.setPrototypeOf(obj, Con.prototype)
  let result = Con.apply(obj, args)
  return result instanceof Object ? result : obj
}
复制代码

通过实现一个简易的new功能,我们知道,new操作符主要做了:

  1. 创建一个新对象。
  2. 把新对象的__proto__属性指向构造函数的原型对象,实现对构造函数原型属性的继承。
  3. 执行构造函数,把this指向新对象,实现对构造函数实例属性的继承。
  4. 判断构造函数执行结果,如果是对象,返回这个对象,否则返回第一步创建的新对象。

class类中的extends继承:

在es6的class类出现之前,JS中实现继承通常使用寄生组合式继承来实现,这种方式步骤复杂、不易理解。class出现后,JS编写继承,就可以像Java中的实现继承一样,步骤清晰而且容易理解。
不过class的本质还是构造函数,extends也是上面说到的继承方式的组合。它可以看作一个语法糖,让对象原型的写法更加清晰、更像面向对象编程的语法。

class和构造函数对比:

先对比一下class和构造函数写法的区别:

  class Person {
    constructor(name) {
      this.name = name
    }
    getName() {
      return this.name
    }
  }
复制代码

上面的代码用构造函数实现:

  function Person(name) {
    this.name = name
  }
  Person.prototype.getName() {
    return this.name
  }
复制代码

通过上面的对比可以看出:

  • class声明语句会定义一个变量Person,并把内部的constructor赋值给Person
  • 在class中定义的方法,实际是绑定到了构造函数的原型对象上

class继承和es5继承对比:

  class Father {
    constructor(name) {
      this.name = name
    }

    getName() {
      return this.name
    }
    static getStatic() {
      console.log('我是一个静态方法')
    }
  }

  class Son extends Father{
    constructor(name, age) {
      super(name)
      this.age = age
    }

    getAge() {
      return this.age
    }
  }
  Son.getStatic() // 我是一个静态方法

  let son = new Son('Tom', 18)
复制代码

上面代码用构造函数实现:

  function extendsPrototype(subType, superType) {
    // 用father的原型,创建一个临时对象
    let prototype = Object.create(superType.prototype)

    // 增强对象,把临时对象的constructor 指向子类构造函数
    prototype.constructor = subType  

    // 子类把原型对象指向 新对象
    subType.prototype = prototype 

    
    subType.__proto__ = superType
  }

  function Father(name) {
    this.name = name
  }
  Father.prototype.getName = function() {
    return this.name
  }
  Father.getStatic = function() {
    console.log('我是一个静态方法')
  }

  function Son(name, age) {
    Father.call(this, name)
    this.age = age
  }

  extendsPrototype(Son, Father)
  Son.prototype.getAge = function() {
    return this.age
  }

  let son = new Son('Tom', 18)
复制代码

通过比较可以看出,es6的class的继承十分类似于寄生组合式继承:

  • 关键字extends是把子类的prototype赋值成了一个继承自父类原型的临时对象,并子类的__proto__赋值成父类(subType.__proto__ = superType)。
  • 调用super,就是调用父类构造函数(Father.bind(this))

但是和寄生组合式继承有一些区别:

  • 寄生组合式继承中,Son.__proto__ === Function.prototype,而class继承中,Son.__proto__ === Father,这也是为什么Son继承了getStatic静态方法的原因。

  • class继承中,在通过super调用父类构造函数之前 不能再构造函数中使用this。为了确保父类先于子类得到初始化。es5中借用构造函数继承,是先创建子类实例的this,在把父类属性方法加到this上。

总结

本文介绍了es5常见的几种继承方式,以及他们的优缺点。并从继承和原型方面分析了new操作符具体做了哪些事情。最后介绍了es6中的class继承方式、和es5继承的区别。

如果文章有什么错误或者大家有什么疑问,欢迎在评论区指出、留言。我们一起学习,一起进步!

Guess you like

Origin juejin.im/post/7086070822179340301