【JavaScript】【ES6】【运算符与语句】class类

112 阅读10分钟

概述

对一类具有共同特征的事物的抽象(构造函数的语法糖)

原理:类本身指向构造函数,所有方法定义在 prototype 上,可看作构造函数的另一种写法(Class === class.prototype.constructor)

本质:function。类的数据类型就是函数,类本身就指向构造函数

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

使用:也是直接对类使用 new 命令,跟构造函数的用法完全一致

class Person {

}
const zhangsan = new Person();

一、Class的基本语法

1.1 类的定义

  • 类声明
class Person {}
// class Person {}

image.png

  • 类表达式
const Person = class {}

image.png

Class 类不存在变量提升,并且类的内部默认就是严格模式

1.2 constructor() 方法

constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。

特点:

  • 一个类必须有 constructor()方法。用户不定义就会被默认添加。示例如下:
class Person {
}

// 等同于
class Person {
  constructor() {}
}
  • constructor()方法默认返回实例对象(即 this),完全可以指定返回另外一个对象
// constructor()函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
class Foo {
  constructor() {
    return Object.create(null);
  }
}

new Foo() instanceof Foo; // false
  • 类必须使用new调用,否则会报错。
class Person {
}

Person()
// TypeError: Class constructor Person cannot be invoked without 'new'

image.png

1.3 类的实例

生成类的实例,需要使用new命令:

class Person {};
var zhangsan = new Person('zhangsan', 18);
  • 类的属性和方法,既可以定义在this对象上,也可以定义在class上
class Person{
    // name 和 age都是定义在this对象上(其自身)
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    // getInfo 是定义在类上的
    getInfo() {
        return `我的名字叫${this.name}, 今年${this.age}岁;`;
    }
}
var zhangsan = new Person('zhangsan', 18);
console.log(zhangsan.getInfo());
console.log(zhangsan.hasOwnProperty('name'))
console.log(zhangsan.hasOwnProperty('getInfo'))
console.log(zhangsan.__proto__.hasOwnProperty('getInfo'))

image.png

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

    它们的原型都是 XXX.prototype

    • __proto__属性是相等的;

    image.png

    • 在其中一个实例上修改原型,其余实例的原型也会随着更新

    image.png

拓展:__proto__属性并不是语言本身的特性,而是各大厂商具体实现时添加的私有属性。

  • 实例属性可以定义在类内部的最顶层。这是ES2022的新写法

优点:实例对象自身的属性都定义在类的头部,看上去比较整齐,一目了然,简洁明了

image.png

1.4 取值函数 getter 和存值函数 setter

类的内部可以使用 getset 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为

image.png

1.5 属性表达式

类的属性名,可以采用表达式

image.png

1.6 Class表达式

类也可以使用表达式的定义

1.7 静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。

静态方法:在方法前面加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用

image.png

  • 如果静态方法包含 this 关键字,这个 this 指的是类,而不是实例。所以静态方法可以与非静态方法重名。

image.png

  • 父类的静态方法,可以被子类继承

image.png

  • 静态方法也是可以从 super 对象上调用的

image.png

1.8 静态属性

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

写法:属性前加上关键字 static

新写法: image.png

旧写法: image.png

1.9 私有方法和私有属性

  • 私有属性 ES2022正式为class添加了私有属性

定义:在属性名之前使用#表示

class Foo {
  publicFieldName = 1;
  #privateFieldName = 2;
}

引用

class Foo {
  publicFieldName = 1;
  #privateFieldName = 2;
  add() {
    return this.publicFieldName + this.#privateFieldName;
  }
  // this.# 简略的写法:
  add1() {
    return this.publicFieldName + #privateFieldName;
  }
}

不管在类的内部或外部,读取一个不存在的私有属性,也都会报错

image.png

私有属性也可以设置 getter 和 setter 方法:

class Counter {
  #xValue = 0;

  constructor() {
    console.log(this.#x);
  }

  get #x() { return this.#xValue; }
  set #x(value) {
    this.#xValue = value;
  }
}

不仅可以利用 this 来引用自己的私用属性,你也可以在类中访问同类其它实例的私有属性

image.png

  • 私有方法

定义:在方法前加#

class Foo {
  #a;
  #b;
  constructor(a, b) {
    this.#a = a;
    this.#b = b;
  }
  // 私有方法
  #sum() {
    return this.#a + this.#b;
  }
  printSum() {
    console.log(this.#sum());
  }
}

私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法。

class FakeMath {
  static PI = 22 / 7;
  static #totallyRandomNumber = 4;

  static #computeRandomNumber() {
    return FakeMath.#totallyRandomNumber;
  }

  static random() {
    console.log('I heard you like random numbers…')
    return FakeMath.#computeRandomNumber();
  }
}


FakeMath.PI // 3.142857142857143
FakeMath.random()

1.10 静态块

ES2022引入静态块(static block)

解决问题:静态属性初始化的问题,之前是在类的外部或constructor()方法里面定义。前者是将类的内部逻辑写到了外部,后者则是每次新建实例都会运行一次。

作用:允许在类的内部设置一个代码块,在类生成是运行且只运行一次。是对静态属性进行初始化.

优点;新建类的实例时,这个块就不运行了

class C {
  static x = ...;
  static y;
  static z;

  // static 代码块,静态块
  static {
    try {
      const obj = doSomethingWith(this.x);
      this.y = obj.y;
      this.z = obj.z;
    }
    catch {
      this.y = ...;
      this.z = ...;
    }
  }
}

静态块内部可以使用类名或this,指代当前类。

class C {
  static x = 1;
  static {
    this.x; // 1
    // 或者
    C.x; // 1
  }
}

注意点:静态块的内部不能有 return 语句

三、class的继承

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

// Point是父类,ColorPoint是子类
// ColorPoint 继承了Point类的所有属性和方法
class Point {
}

class ColorPoint extends Point {
}

上面示例中,但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。

ES6规定:子类必须在constructor()方法中调用super(),否则就会报错。

理由:因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()方法,子类就得不到自己的this对象。 为什么一定要调用super():在于ES6的继承机制,是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。 拓展 - ES5的继承机制:是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。

继承规则

  • 父类的私有属性和私类方法不会被子类继承

  • 父类的静态属性和静态方法可以被子类继承

    静态属性是通过软拷贝(浅拷贝)实现的

    class A { static foo = 100; }
    class B extends A {
    constructor() {
        super();
        B.foo--;
    }
    }
    
    const b = new B();
    B.foo // 99
    A.foo // 100
    
  • Object.getPrototypeOf()方法可以用来从子类上获取父类。(PS: 可以使用这个方法判断,一个类是否继承了另一个类。)

    class Point { /*...*/ }
    
    class ColorPoint extends Point { /*...*/ }
    
    Object.getPrototypeOf(ColorPoint) === Point
    // true
    

2.1 super关键字

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

  • 作为函数调用。代表父类的构造函数

ES6要求:子类的构造函数必须执行一次super()函数。否则报错。

调用super()作用:形成子类的this对象,把父类的实例属性和方法放到这个this对象上。

class A {}

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

注意点

  • 由于super()在子类构造方法中执行时,子类的属性和方法还没有绑定到this,所以如果存在同名属性,此时拿到的是父类的属性。
  • super()只能用在子类的构造函数之中,用在其他地方就会报错。

image.png

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

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

image.png

上面示例中,子类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
  • 属性定义在父类的原型对象上,super就可以取到。
class A {}
A.prototype.x = 2;

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

let b = new B();

2.2 类的 prototype 属性和__proto__属性

类同时存在两条继承链:

  • 子类的__proto__属性,表示构造函数的继承,总是指向父类
  • 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {
}

class B extends A {
}

// 子类B的__proto__属性指向父类A
B.__proto__ === A // true
// 子类B的prototype属性的__proto__属性指向父类A的prototype属性
B.prototype.__proto__ === A.prototype // true

子类的原型的原型,是父类的原型:

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

// 因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为。
p2.__proto__.__proto__.printName = function () {
  console.log('Ha');
};

p1.printName() // "Ha"

2.3 原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。

ECMAScript 的原生构造函数大致有下面这些:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • Date()
  • RegExp()
  • Error()

注意点:生构造函数是无法继承的。

2.4 Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。

四、总结

  • 方法和关键词:

    • constructor: 构造函数,new命令 生成实例时自动调用
    • extends: 继承父类
    • super: 新建父类的 this
    • static: 定义静态属性方法
    • get: 取值函数,拦截属性的取值行为
    • set: 存值函数,拦截属性的存值行为
  • 属性

    • proto构造函数的继承(总是指向父类)
    • proto.proto :子类的原型的原型,即父类的原型(总是指向父类的__proto__)
    • prototype.proto属性方法的继承(总是指向父类的prototype)
  • 继承

    • 实质
      • ES5:先创造子类实例的 this,再将父类的属性方法添加到this上(parent.apply(this))
      • ES6:先将父类实例的属性方法加到 this 上(调用super()),再用子类构造函数修改this
    • super
      • 作为函数调用:只能在构造函数中调用 super(),内部 this 指向继承的 当前子类(super()调用后才可在构造函数中属于this)
      • 作为对象调用:在 普通方法 中指向 父类的原型对象,在 静态方法 中指向 父类
    • 显示定义:使用 constructor() { super(); } 定义继承父类,没有书写 显示定义
    • 子类继承父类:子类使用父类的属性方法时,必须在构造函数中调用super(),否则得不到父类的this
      • 父类静态属性方法可被子类继承
      • 子类继承父类后,可从super上调用父类静态属性方法
    • 实例:类相当于实例的原型,所有在类中定义的属性方法都会被实例继承
      • 显式指定属性方法:使用this指定到自身上(使用Class.hasOwnProperty()可检测到)
      • 隐式指定属性方法:直接声明定义在对象原型上(使用Class.__proto__.hasOwnProperty()可检测到)
    • 表达式
      • 类表达式:const Class = class {}
      • name属性:返回紧跟class后的类名
      • 属性表达式:[prop]
      • Generator方法:* mothod() {}
      • Async方法:async mothod() {}
    • this指向:解构实例属性或方法时会报错
      • 绑定this:this.mothod = this.mothod.bind(this)
      • 箭头函数:this.mothod = () => this.mothod()
    • 属性定义位置
      • 定义在构造函数中并使用this指向
      • 定义在类最顶层
    • new.target:确定构造函数是如何调用

参考: