ES6 之 Class

109 阅读6分钟

基本语法

定义类

Class 类是 ES6 新增的,在之前本身是不具有类的概念的。都是通过构造函数来模拟面对对象的。

function Point(x,y){
  this.x = x
  this.y = y
}

Point.prototype.toString = function(){
  return `(${this.x}, ${this.y})`
}

let point = new Point(1,2)

即使是ES6 的class也可以看作只是一个语法糖。

class Point {

  constructor(x,y){
    this.x = x
    this.y = y
  }

  toString(){
    return `(${this.x}, ${this.y})`
  }
}

let point = new Point(1,2)
console.log(point);
console.log(typeof Point); // function
console.log(Point === Point.prototype.constructor); // true

通过关键字class定义类,里面的constructor()方法就是构造方法,用来初始化类的。这里的this指向由当前类实例的对象。

类的数据类型是function,依然有原型对象,且原型对象的constructor属性执行类本身。使用的时候也是通过new的,和构造函数一致。

image.png

constructor() 方法

constructor()方法是类的默认方法,当new一个类时,会自动调用该方法,如果未定义的话,相当于调用一个空的constructor()constructor()方法默认返回实例对象(this)

class Point{}

// 等同于
class Point{
  constructor(){
    return this
  }
}

constructor()函数返回一个新的对象,导致实例对象不是Point类的实例。

class Point{
  constructor(){
    return Object.create(null)
  }
}

let point = new Point()
console.log(point instanceof Point);  // false

类的实例

生成类的实例必须通过new来调用,否则报错。
类的方法和属性除非是定义在this对象上,否则都是定义在class上(原型上)

class Point{
  constructor(x,y){
    this.x = x
    this.y = y
  }

  toString(){
    return `(${this.x}, ${this.y})`
  }
}

let point = new Point(1,2)

console.log(point.toString()); // (1, 2)

console.log(point.hasOwnProperty('x'));  // true
console.log(point.hasOwnProperty('y')); // true
console.log(point.hasOwnProperty('toString')); // false
console.log(point.__proto__.hasOwnProperty('toString')); // true

上面代码,xy都是实例对象point上(this对象),所以hasOwnProperty()方法返回true,而toString()方法则返回false

类的所有实例共享一个原型对象。

let p1 = new Point(1, 2)
let p2 = new Point(2, 3)

console.log(p1.__proto__ === p2.__proto__) // true

p1.__proto__.range = function () {
  return this.x * this.y
}

console.log(p1.range());  // 2
console.log(p2.range());  // 6

上面代码中,p1p2都是Point的实例,它们的原型指向相同。在p1原型上定义的方法,p2同样可以调用

实例属性

class中显示声明属性,不用在构造函数中创建定义

class Person {
  name
  age = 0
  gender
  hobby

  constructor(name, gender) {
    this.name = name
    this.gender = gender
  }
}

let person = new Person('jack', '男')
console.log(person.name) // jack
console.log(person.age) // 0
console.log(person.gender) // 男
console.log(person.hobby) // undefined

class顶部显示定义了属性,依然可以在constructor()方法中设置属性的值(设置name的值为jack)。属性可以设置默认值,不设置就是undefined

静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

class Person{
  static strict = true
}

Person.type = 1
console.log(Person.type); // 1

console.log(Person.strict);  // true

目前有两种写法,一种是通过static关键字,另一种是直接在类上定义属性。

私有属性

私有属性是通过在属性名之前使用#定义。私有属性只能在类的内部使用,外部调用会报错

class Counter {
  #count = 0

  get value() {
    return this.#count;
  }

  increment() {
    this.#count++;
  }

  getValue(){
    return this.#count
  }
}

let counter = new Counter()

// console.log(counter.#count);  // 报错
counter.increment()
console.log(counter.getValue()); // 1

Getter 和 Setter

class Counter{
  _count = 0

  get count(){
    return this._count
  }

  set count(value){
    this._count = value
  }
}

let counter = new Counter()

counter.count = 10
console.log(counter.count);  // 10

属性表达式

类的属性名,可以采用表达式进行动态设置

let methodName = 'getArea'

class Square {
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  [methodName]() {
    return this.x * this.y
  }
}

let square = new Square(1, 2)
console.log(square.getArea()) // 2
console.log(square[methodName]()) // 2
console.log(square['getArea']()) // 2

Class 表达式

const myCounter = class Counter{
  getClassName() {
    return Counter.name;
  }
}

let counter = new myCounter()
console.log(counter.getClassName());  // Counter
console.log(Counter.name); // 报错 Counter is not defined
console.log(myCounter.name); // Counter

Counter只能在Class内部使用。在Class外部这个类只能通过myCounter调用

立即执行的 Class。

const myClass = new class{
  toString(){
    console.log('Class');
  }
}()

myClass.toString() // Class

实例方法

在类中定义的方法会被实例继承,可以访问和修改实例数据。实例方法可以调用其它实例方法和静态方法

class Person {
  name = ''

  constructor(name) {
    this.name = name
  }

  getName() {
    return this.name
  }

  printName() {
    console.log(`my name is ${this.getName()}`)
  }
}

let p = new Person('jack')
console.log(p.getName()) // jack

p.printName() // my name is jack

静态方法

在一个方法前,加上static关键字,就表示该方法不会被实例继承,只能直接通过类来调用。

  1. 静态方法可以访问静态字段
  2. 静态方法无法访问实例字段
  3. 静态方法中的this指向的是类
  4. 静态方法可以与非静态方法重名,静态调用时调用静态重名方法,实例调用时调用实例重名方法
class Person {
  static name = 'Unknown'

  static getName() {
    return this.name
  }

  static a(){
    this.b()
  }

  static b(){
    console.log('b');
  }

  b(){
    console.log('bb');
  }
}

let p = new Person('jack')

// console.log(p.getName()) // p.getName is not a function
console.log(Person.getName()); // Unknown
Person.a() // b
p.b() // bb

私有方法

私有方法是只能在类的内部访问的方法。

class Counter {
  a = 1
  #b = 2

  #printB(){
    console.log(this.#b);
  }

  print(){
    this.#printB()
  }
}

let counter = new Counter()
counter.print() // 2

静态私有方法和静态私有属性

静态的私有属性或私有方法只能在内部调用

class Counter {
  static #a = 1

  static #printA(){
    console.log(Counter.#a);
  }

  static printA(){
    Counter.#printA()
  }
}

Counter.printA() // 1

运算符

in 运算符

in运算符判断某个对象属性是否存在。判断私有属性时,in只能用在类的内部。

class A {
  #foo = 0;

  static isA(obj) {
    if (#foo in obj) {
      return true;
    } else {
      return false;
    }
  }

  m() {
    console.log(#foo in this); // true
    console.log(#bar in this); // false
  }
}

instanceof 运算符

instanceof运算符判断对象是否为类的实例

class Person {
  name

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

let p = new Person('jack')
let obj = {}

console.log(p instanceof Person) // true
console.log(obj instanceof Person) // false

继承

Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。

新建子类实例时,父类的构造函数必定会先运行一次,必须调用super()

class A {
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  toString() {
    console.log(`${this.x},${this.y}`)
  }
}

class B extends A {
  constructor(x, y) {
    super(x, y) // 调用父类的constructor(x, y)
  }
}

let b = new B(1, 2)

b.toString() // 1,2

console.log(b instanceof A) // true
console.log(b instanceof B) // true

私有属性和私有方法的继承

子类无法继承父类的私有属性和私有方法,可以通过在父类定义普通方法来调用私有属性和方法。

class A {
  #a = 2

  #printA(){
    console.log(this.#a);
  }

   print(){
    this.#printA()
  }
}

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

  printB(){
    this.#printA() // 报错 在类 "A" 外部不可访问
  }
}

let b = new B()

console.log(b);
console.log(b.#a); // 报错 在类 "A" 外部不可访问
b.print() // 2

静态属性和静态方法的继承

父类的静态属性和静态方法,会被子类继承。静态属性是通过软拷贝实现继承的。

class A {
  static count = 1
  static obj = {
    name:'A'
  }

  static printA(){
    console.log(A.obj.name);
  }
}

class B extends A {
  constructor(){
    super()
    B.count--
    B.obj.name = 'B'
  }
}

A.printA() // A
let b = new B()
console.log(B.count); // 0
console.log(A.count); // 1
B.printA()  // B
A.printA() // B

A类中定义的基础属性count,在B类内部修改B.count影响不到A.count。因为继承静态属性,是通过浅拷贝复制的属性值,因为A.countB.count是两个独立的属性。
B类内部修改B.obj.name影响到了A.obj.name,是因为浅拷贝复制了引用地址。

Object.getPrototypeOf()

Object.getPrototypeOf()方法可以用来从子类上获取父类。

class A {  }

class B extends A {  }

console.log(Object.getPrototypeOf(B));  // class A {  }
console.log(Object.getPrototypeOf(B) === A); // true

new.target 属性

new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

// 另一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三');  // 报错

Class 内部调用new.target,返回当前 Class。如果是子类继承父类时,new.target会返回子类。

class A {
  constructor(x) {
    console.log(new.target === A);
    this.x = x;
  }
}

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

var a = new A(3);  // true

var b = new B(1) // false

利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。

class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('本类不能实例化');
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super();
    // ...
  }
}

var x = new Shape();  // 报错
var y = new Rectangle(3, 4);  // 正确

super 关键字

super关键字有两种使用方式,当作函数使用或者对象使用。

作为函数调用时,super代表父类的构造函数。子类的构造函数必须执行一次super()函数。

class A {}

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

调用super()就是调用父类的构造函数生成子类的this对象,把父类的实例属性和方法添加到this对象。然后子类可以在这个this对象上进行操作。

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

class A {

  static printA(msg) {
    console.log('static', msg);
  }

  printA(msg) {
    console.log('instance', msg);
  }
}

class B extends A {

  static printB(msg) {
    super.printA(msg); // 等同于 A.printA()
  }

  printB(msg) {
    super.printA(msg); // 等同于 A.prototype.printA()
  }

  static print(){
    super.print()
  }
}

B.printB(1); // static 1

var b = new B();
b.printB(2); // instance 2

类的 prototype 属性和__proto__属性

Class 类作为构造函数的语法糖同样拥有prototype属性和__proto__属性

  1. 子类的__proto__属性,表示构造函数的继承,总是指向父类。
  2. 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {
  a = 1

  printA() {
    console.log(this.a)
  }
}

class B extends A {
  b = 2

  printB() {
    console.log(this.b)
  }
}

console.log(B.__proto__) // class A
console.log(B.__proto__ === A) // true

console.log(B.prototype.__proto__) // class A prototype
console.log(B.prototype.__proto__ === A.prototype) // true