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

293 阅读6分钟

深入理解面向对象-ES6的类和类的继承

前言

ES6之前,按照构造函数的形式创建的,不仅仅和编写普通函数过于相似,而且代码并容易理解。

  • 在es6(ECMAScript2015)新的标准中,使用了class关键字来直接定义类 但是类本质上依然是构造函数、原型链的语法糖而已

使用class来定义一个类

定义一个类有两种方式:类声明类表达式

    //类声明
    class Person{}
    //类表达式
    let student = class{}

类和构造函数的异同

类和构造函数的特性其实是一致的

 let p = new Person()
 console.log(p.__proto__ === Person.prototype) // true
 console.log(Person) // [class Person]
 console.log(Person.prptotype)   // {}
 console.log(Person.prototype.__proto__) //[Object: null prototype] {}
 console.log(Person.prototype.constructor) // [class Person]
 console.log(typeof Person) // function 

类的构造函数

如果我们希望在创建对象的时候给类传递一些参数,这个时候该如何做呢?

  • 每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor
  • 当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor
  • 每个类只能有一个构造函数,如果包含多个构造函数就会抛出异常 当我们通过new关键字操作类的时候,就会调用这个constructor函数
    class person {
        constructor(name,age,height){
            this.name =name
            this.age = age
            this.height = height
        }
     }

类的实例方法

在上面我们定义的属性都是直接放到了this上,也就意味着他是放到创建出来的新对象中:

  • 在前面我们对于实例的方法,希望是放到原型上卖弄,这样就可以被多个实例来共享
  • 这个时候就可以直接在类里面定义
 class person {
      constructor(name,age,height){
           this.name =name
           this.age = age
           this.height = height
        }
        running(){
            console.log(this.name + "running~")
        }  
        eating(){
            console.log(this.name + "eating~")
        }
  }

类的访问器方法

对象可以添加gettersetter函数,类同样也可以添加

class Person {
    contructor(name){
        this._name = name
     }
     get name(){
         console.log("调用了name的getter方法")
         return this._name
     }
     set name(newName){
         console.log("调用了name的setter方法")
         this._name = newName
     }
}

类的静态方法

静态方法常用于直接使用类来执行的方法,不需要类的实例,使用static关键字来定义

class Person {
    contructor(age){
        this.age = age
     }
     
     static create(){
         return new Person(Math.floor(Math.random() * 100))
     }
}     
//使用类的静态方法
Person.create()  

ES6类的继承-extends

ES5中实现的继承方法是寄生组合式继承,虽然实现的继承的效果,但过程是非常繁琐的,ES6中新增了使用extends关键字,可以方便我么能实现继承:

class Person{}

//继承
class Student extends Person {}

super 关键字

super关键字,既可以当作函数使用,也可以当作对象使用,在这两种情况下用法完全不相同

super当作函数调用

super当作函,数调用时,代表父类的构造函数,ES6要求,子类的构造函数必须执行一次super函数

calss A{}

class B extends A {
    constructor(){
        super()
     }
}

上面代码中,子类B的构造函数中的super(),代表父类A的构造函数。 注意:super虽然代表了父类A的构造函数,但返回的是子类B的实例,即super内部的this指向B的实例,因此super()在这里相当于A.prototype.constructor.apply(this)

作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错

class A {}

class B extends A {
  m() {
    super(); // 报错
  }
}

上面代码中,super()用在B类的m方法之中,就会造成语法错误。

super作为对象

super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类

class A{
    p(){
        return 2
    }   
}
class B extends A {
  constructor() {
      super();
      console.log(super.p()); // 2
  }
}

let b = new B()

上面代码中,子类B当中的super.p(),就是super作为对象使用。这时super在普通方法中,指向A.prototype,所以super.p()就相当于A.prototype.p()。 这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

上面代码中,p是父类A实例的属性,super.p就引用不到它。

如果属性定义在父类的原型对象上,super就可以取到。

class A {}
A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}

let b = new B();

ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2

上面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)

由于this指向子类实例,所以如果通过super对某个属性赋值,这是super就是this,赋值的属性就会变成子类的实例的属性

class A {
  constructor() {
    this.x = 1;
  }
}
class B extends A {
    constructor(){
        super()
        this.x = 2
        super.x = 3
        console.log(super.x) //undefined
        console.log(this.x) // 3
    }
}    

上面代码中,super.x赋值为3,这是等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined

如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。

class Parent {
    static myMethod(msg){
        console.log('static',msg)
     }
     myMethod(msg){
         console.log('instance',msg)
     }
}
class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }

  myMethod(msg) {
    super.myMethod(msg);
  }
}

Child.myMethod(1)   // static 1

var child = new Child()
Child.myMethod(1)   // instance 2

上面代码中,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。

class A {
  constructor() {
    this.x = 1;
  }
  static print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static m() {
    super.print();
  }
}

B.x = 3;
B.m() // 3

上面代码中,静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例。

注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

ES6继承实现代码

//父类
class Person {
    constructor(name,age){
        this.name =name
        this.age = age
     }
    running() {
        console.log(this.name + " running~")
    } 
    eating() {
        console.log(this.name + " eating~")
    }
    personMethod() {
        console.log("处理逻辑1")
        console.log("处理逻辑2")
        console.log("处理逻辑3")
    }
    static staticMethod() {
        console.log("PersonStaticMethod")
    }
}
//子类
class Student extends Person {
    constructor(name,age,sno){
        super(name,age)
        this.sno = sno
     }
     studying() {
        console.log(this.name + " studying~")
     }
     //类对父类方法的重写
     running(){
          console.log("student " + this.name + " running")
     }
     //对personMethod方法的重写,复用一部分父类的处理逻辑
     personMethod() {
        super.personMethod()
        console.log("处理逻辑4")
        console.log("处理逻辑5")         
     }
     // 重写静态方法
     static staticMethod() {
         super.staticMethod()
         console.log("StudentStaticMethod")
      }
}
var stu = new Student("why", 18, 111)
console.log(stu)  // Student { name: 'why', age: 18, sno: 111 }
stu.eating() // why eating~
stu.running()  //student why running
stu.personMethod()  // 处理逻辑1 ~ 处理逻辑5
Student.staticMethod()  //PersonStaticMethod StudentStaticMethod