ES6中的类(详细版)

类的初识

  1. class是ES6中新的基础性语法糖,表面上可以支持正式的面向对象编程,但实际背后仍使用的是原型和构造函数的概念

  2. 类的首字母大写,区别其创造的实例:通过class Student{}创建实例student

  3. 类实质上就是一个函数。类自身指向的就是构造函数。所以可以认为ES6中的类其实就是构造函数的另外一种写法

class Person {} 

console.log(typeof Person) // function
console.log(Person === Person.prototype.constructor) // true
复制代码

类的构造函数

constructor关键字

  1. constructor关键字告诉解释器,使用new操作符创建类实例时,应该调用该函数。即使类中无constructor,也会自动生成该构造函数

  1. 类的属性的定义位置是有区别的
  • constructor中定义的属性可以称为实例属性(即定义在this对象上)。 每一个实例都对应一个唯一的成员对象,这意味着实例属性不会在原型上共享
  • constructor外声明的属性,包括函数都是定义在原型上的,可以称为原型属性或者原型方法(即定义在class上,可以在实例中的__proto__中看到)
 class Person {
    constructor(name,age){
      this.name = name
      this.age = age
      this.read = function read(){
        console.log(`阅读`);
      }
    }
    speak(){
      console.log(`我叫${this.name}`);
    }
  }
  let person = new Person('Lee',22)
  console.log(person.hasOwnProperty("speak")) // false
  console.log(person.hasOwnProperty("read")) // true
  console.log(person.__proto__.read) // undefined
  console.log(person)
复制代码

image.png

构造函数

  1. 使用new调用Person的constructor后发生了什么?

内存中创建一个新对象 ——> person.__ proto __ = Person.prototype ——> 构造函数内部this指向新对象,然后执行构造函数的代码,给新对象添加属性 ——> 返回新对象

  1. 类的__ proto __ 属性 (上述过程中的第二步与此有关)
  • 实际上,类的所有实例共享一个原型对象,它们的原型都是Person.prototype,所以proto属性是相等的
class Person {
    constructor(name,age){
      this.name = name
      this.age = age
      this.read = function read(){
        console.log(`阅读`);
      }
    }
    speak(){
      console.log(`我叫${this.name}`);
    }
  }
  let person = new Person('Lee',22)

  // person.__proto__就是Person.prototype
  // 里面有constructor: class Person和speak: ƒ speak()
  console.log(person.__proto__)

  // Person {name: "LeeCopy", age: 23, read: ƒ}
  let person2 =  new person.__proto__.constructor('LeeCopy',23)
  console.log(person2)
  
  // true
  console.log(person.__proto__ === person2.__proto__)
复制代码

涉及到原型链的知识,如下图所示(借用红宝书)

image.png

  • 使用实例的proto属性改写原型,会改变Class的原始定义,影响到所有实例
  class Person {
    constructor(name,age){
      this.name = name
      this.age = age
      this.read = function read(){
        console.log(`阅读`);
      }
    }
    speak(){
      console.log(`我叫${this.name}`);
    }
  }

  let person = new Person('Lee',22)
  let person2 =  new Person('LeeCopy',23)
  
  // 不能用箭头函数,因为这样的话this指向的是window
  person.__proto__.sayAge = function () {console.log(this.age)}
  person.__proto__.speak = null

  person2.sayAge() // 23
  person2.speak() // Uncaught TypeError: person1.speak is not a function
复制代码

类的继承

类继承虽然使用的是新语法,但是背后仍使用的是原型链。 使用extends关键字就可以继承任何拥有constructor和prototype的对象,这意味着不仅可以继承类,也可以继承构造函数

类继承的语法

  1. 子类必须在constructor方法中调用super方法之后,才能使用this关键字

这是因为子类没有自己的this对象,而是继承父类的this对象。如果不调用super方法,子类就得不到this对象。由此,子类在constructor函数中需要先添加super(),再定义自己的属性

  // 报错:Must call super constructor in derived class before accessing
  // 没有super()
  class Son extends Father {
    constructor(sonName, sonAge) {
      this.sonName = sonName;
      this.sonAge = sonAge
    }
  }
  
  // 报错: Must call super constructor in derived class before accessing
  // this比super()早,此时子类还没有自己的this对象
  class Son extends Father{
    constructor(sonName,sonAge,school) {
      this.school = school
      super(sonName,sonAge);
    }
  }
复制代码

在这一点上ES5的继承与ES6正好相反,ES5先创建自己的this对象然后再将父类的属性方法添加到自己的this当中

  1. 如果子类没有显式地定义constructor,代码将被默认添加。换言之,如果constructor函数中只有super的话,该constructor函数可以省略
// 如果不想给子类添加新属性,下面两个写法效果一样
  class Son extends Father{}

  class Son extends Father{
    constructor(sonName,sonAge) {
      super(sonName,sonAge);
    }
  }
复制代码

继承中的就近原则(优先使用子类方法)

  class Father {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }
    sayMe() {
      console.log(`我是老爸,我叫${this.name},年龄${this.age}`)
    }
  }
  
  class Son extends Father{
    constructor(sonName,sonAge,school) {
      super(sonName,sonAge);
      this.school = school
    }
    sayMe() {
      console.log(`我覆盖了老爸的方法,我叫${this.name},年龄${this.age}`)
      super.sayMe()
    }
  }

  let son = new Son('Lee的son', 23)
  // 我覆盖了老爸的方法,我叫Lee的son,年龄23
  // 我是老爸,我叫Lee的son,年龄23
  son.sayMe()
复制代码

类继承的本质

  1. ES6的class继承

我们先来看一下用ES6的class实现继承后的情况

class Father {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }
    fatherSay() {
      console.log(`father`)
    }
  }

  class Son extends Father{
    constructor(sonName,sonAge,school) {
      super(sonName,sonAge);
      this.school = school
    }
    sonSay() {
      console.log(`son`)
    }
  }
  let son1 = new Son('Lee的son', 23, '南邮')
  console.log(son1)
  // son1.__proto__指向Father实例,相当于Son.prototype = new Father()
  // 但是son1.__proto__的constructor指回了Son
  console.log(son1.__proto__)
复制代码

image.png

我们可以很清楚地看到,ES6的class实现继承依旧是使用的是ES5原型链实现继承,细节上略有不同,后文会补充不同之处

  1. ES5构造函数实现继承

我们再来看一下ES5实现继承的方式

function Father(name,age){
       this.name = name;
       this.age = age;
       this.fatherSay = function (){
             console.log(`father`)
           }
     }

     function Son(sonName,sonAge,school) {
       Father.call(this,sonName,sonAge)
       this.school = school
       this.sonSay = function (){
              console.log(`son`)
            }
     }
     Son.prototype = Object.create(Father.prototype)
     Son.prototype.constructor = Son

     let son1 = new Son('Lee的son', 23, '南邮')
     console.log(son1)
     console.log(son1.__proto__)
     console.log(son1.__proto__.constructor) // 指回构造函数
复制代码

image.png

可以看到类继承和构造函数继承有异曲同工之处

不同之处红线部分已标出 —— 构造函数继承中的函数都为实例属性或实例函数,本来实例属性不会在原型上共享的,但是在继承中,父类实例属性摇身一变变成了原型属性,以此给子类复用,变成了子类的实例属性

  • 补充: 继承中的原型链

原型链的基本思想就是通过原型继承多个引用类型的属性和方法,子的原型是父实例,意味着子原型本身有一个内部指针指向另一个原型,相应的父原型也有一个指针指向另一个构造函数,这样实例和原型之间就构造了一条原型链。

image.png

类继承的应用

  • 继承内置引用类型,扩展内置类型
// randomArr可以创建一个数组,该数组内置了洗牌算法
  class randomArr extends Array {
    shuffle() {
      for (let i = this.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 2));
        [this[i], this[j]] = [this[j], this[i]];
      }
    }
  }
  let arr = new randomArr(1, 2, 3, 4, 5)
  console.log(arr) // [1, 2, 3, 4, 5]
  arr.shuffle()
  console.log(arr) // [5, 4, 2, 3, 1]
  console.log(arr instanceof randomArr) // true
  console.log(arr instanceof Array) // true
复制代码

类的静态方法

静态方法:不需要实例化类,即可直接通过该类来调用的方法,在原型方法前加上static关键字即成静态方法

特点

  1. 可直接通过该类来调用的方法,且不会被实例继承
class Father {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }

    static fatherSay() {
      console.log(`father`)
    }
  }

  Father.fatherSay() // father

  let father = new Father('Lee', 23)
  // Uncaught TypeError: father.fatherSay is not a function
  father.fatherSay()
复制代码

2. 静态方法只能被 静态方法/类 调用

class Father {
    constructor(name, age) {
      this.name = name;
      this.age = age;
    }

    static fatherSay() {
      console.log(`father`)
    }
    static useStatic () {
      this.fatherSay()
    }
    
    failUseStatic (){
      this.fatherSay()
    }
  }

  Father.useStatic() // father

  let father = new Father('Lee', 23)
  // Uncaught TypeError: this.fatherSay is not a function
  father.failUseStatic()
复制代码

3. 父类的静态方法可以被子类继承,子类使用该静态方法和父类一样。 类的静态方法调用父类的静态方法,需要从super对象上调用

  class Father {
    static fatherSay() {
      console.log(`father`)
    }
  }

  class Son extends Father {
    static sonSay() {
      super.fatherSay()
    }
  }

  Son.sonSay() // father
复制代码

应用

  • 静态方法非常适合作为实例工厂
class Person {
    constructor(age) {
      this.age = age
    }
    static create () {
      return new Person(Math.floor(Math.random()*100))
    }
  }
  person1 = Person.create()
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享