类 -- Typescript基础篇(9)

180 阅读4分钟

js并不同于传统面向对象的语言,它使用构造函数和原型链实现面向对象编程。而且并没有真正的类的概念,即使ES6新增了class关键字,它本质也是语法糖。

但是通过使用class能增加代码语义性。并且ts对class提供了诸多功能,使得我们能够更像其他面向对象的语言一样使用class

ts中的class如:

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    console.log("Hello, " + this.greeting);
  }
}

const greeter = new Greeter("world");
greeter.greet();

Greeter类具有一个constructor构造函数,一个greeting属性,一个greet方法。并且使用new关键字生成一个新的实例。

如果开启了strictNullChecksstrictPropertyInitialization(两者需要同时开启),所有的属性必须被初始化,或是在属性声明时,或是在构造函数中:

class Greeter {
  greeting: string = "default";
  constructor() {}
}

// 或者
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
}

否则将会报错:

properties-initialization-error

一个类必须有constructor方法,如果没有显示定义,则会被自动添加一个空的constructor方法:

class Greeter {
}

// 等价于
class Greeter {
  constructor() {
  }
}

继承

使用extends关键字实现继承(与接口不同,只能单继承),子类中使用super关键字调用父类的构造函数和方法:

class Point {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
  getValue() {
    return [this.x, this.y];
  }
}

class ColorPoint extends Point {
  color: string;
  constructor(x: number, y: number, color: string) {
    super(x, y);
    this.color = color;
  }
  getColor() {
    return this.color;
  }
}

const p = new ColorPoint(1, 1, "red");
console.log(p.getValue()); // [1,1]
console.log(p.getColor()); // "red"

注意:

  • contructor中,必须先调用super方法。这是因为子类的this对象必须先通过父类构造函数完成塑造,得到父类同样的实例属性和方法,然后在对其加工,加上子类的实例属性和方法。如果不调用super方法,子类就得不到this对象
  • 基于上一点,在子类的构造函数中,只有调用super之后,才可以使用this关键字

如果子类没有显示的constructor方法,也会被自动添加一个constructor方法,默认包含super()方法:

class Point {
 
}

class ColorPoint extends Point {
}

// 等价于
class ColorPoint extends Point {
   constructor() {
    super();
  }
}

如果我们希望实现的类能够链式调用,如new Animal().setName("name").setColor("red"),那么实现的代码:

class Animal {
  name?: string;
  color?: string;
  setName(name: string): Animal {
    this.name = name;
    return this;
  }
  setColor(color: string): Animal {
    this.color = color;
    return this;
  }
}

如果我们有一个子类继承Animal:

class Bird extends Animal {
  other: any;
  setOther(other: any): Bird {
    this.other = other;
    return this;
  }
}

此时实例化Bird,在调用了setName后,不能调用setOther。这是因为setName返回类型是AnimalAnimal没有setOther方法。但实际上此时实例化的是Bird属性,所以这是不合理的。

class-this

我们可以把所有的返回this函数的返回值类型也指定this,如:setOther(other: any): this。此时ts就会根据我们实例化的类推断出当前this具体类型。

访问限定符

访问限定符用于限定类中属性和方法的可访问范围。在ts中有三种访问限定符:publicprivateprotected

  • public:所修饰的属性或者方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是public
  • private:所修饰的属性或者方法是私有的,只能在类内部被访问,不能在类的外部或者子类被访问
  • protected:所修饰的属性或者方法是受保护的,能在类内部或者子类被访问,不能在类的外部被访问

上面所有的例子都是使用public限定符(默认行为),所以类的方法和属性可以被实例化的对象在外部访问或者被继承的子类访问,没有限制。

如果使用private限定符:


class Animal {
  private name: string;
  constructor(name: string) {
    this.name = name;
  }

  run() {
    console.log(`${this.name} is running`);
  }
}

const animal = new Animal("xiao bai");
animal.run(); // xiao bai is running

// Property 'name' is private and only accessible within class 'Animal'.
animal.name;

// 在子类中也无法访问私有对象
class Cat extends Animal {
  constructor(name: string) {
    super(name);
  }

  getName() {
    // Property 'name' is private and only accessible within class 'Animal'.
    this.name;
  }
}

虽然无法直接访问private属性或者方法,但我们还是可以通过使用索引来绕过限制,达到在外部访问私有成员的效果

// 还是上面的例子,改写访问方式即可成功访问
console.log(animal["name"]);

class Cat extends Animal {
  /*
  ......
  */
  getName() {
    return this["name"];
  }
}

因此,在3.8中新支持了语法,使用#前缀修饰私有成员,实现更为严格的访问限制。

class Square {
  #sideLength: number;
  constructor(length: number) {
    this.#sideLength = length;
  }
  getArea() {
    return this.#sideLength * this.#sideLength;
  }
}

const s = new Square(10);
console.log(s.getArea());
// #sideLength"' can't be used to index type 'Square'
// console.log(s["#sideLength"]);

使用#关键字为前缀的属性会被识别为私有属性,并且可以避免private的规避方法。有几点需要注意:

  • #本质是使用WeakMap实现,需要将编译的target设置为ES6以上,而private对所有target都兼容(官网对此的解释是:WeakMapES6新的数据类型,并且WeakMap不能以不会引起内存泄漏的方式进行polyfill
  • 使用了#前缀的属性,不能再使用访问修饰符限定
  • 只有属性可以使用#,方法不行
  • 使用#的子类属性不会覆盖父类同名属性

针对最后一点进行说明:

class C {
  #foo = 10;
  cHelper() {
      return this.#foo;
  }
}

class D extends C {
  #foo = 20;
  dHelper() {
      return this.#foo;
  }
}

const instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'

如果实现private,则会被被覆盖:

class C {
    foo = 10;
    cHelper() {
        return this.foo;
    }
}

class D extends C {
    foo = 20;
    dHelper() {
        return this.foo;
    }
}

const instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // '20'
console.log(instance.dHelper()); // '20'

如果我们将name的限定符改为protected,则:

class Animal {
  protected name: string;
 /*
 ......
 */
}

const animal = new Animal("xiao bai");
// Property 'name' is protected and only accessible within class 'Animal' and its subclasses.
console.log(animal.name);

class Cat extends Animal {
   /*
 ......
 */
  getName() {
    // 能够访问到父类的受保护属性
    return this.name;
  }
}

当我们使用访问限定符时,类的初始化可以更简洁:

class Greeter { 
  constructor(public greeting: string) {
    this.greeting = greeting;
  }
}

// 等价于
class Greeter {
  greeting: string;
  constructor(greeting: string) {
    this.greeting = greeting;
  }
}

不限于publicprivate或者protected都可以,但需要显式写出限定符关键字,即使是public

只读属性

我们可以使用readonly关键字定义只读属性,只读属性只能在声明时或者构造函数中被赋值。其使用方法和接口的只读属性十分相似:

class Animal {
  public readonly name: string;
  constructor(name: string) {
    this.name = name;
  }
}

let a = new Animal1("Jack");
console.log(a.name); // Jack

// Cannot assign to 'name' because it is a read-only property
a.name = "Tom";

可选属性

类中的属性也可以设为可选属性,可选属性即使不初始化,或者赋值为undefined都合法。使用方式与接口一致,使用?关键字:

class Animal {
  name?: string;
  // 或者
  // name?: string = undefined;
  constructor() {
  }
}

存取器

ts支持使用gettersetter改变属性的读取和赋值行为:

class Employee {
  #fullName: string;
  constructor(name: string) {
    this.#fullName = name;
  }
  get fullName() {
    return this.#fullName;
  }

  set fullName(name: string) {
    if (name.length > 0) {
      this.#fullName = name;
    }
  }
}

const employee = new Employee("li hua");

console.log(employee.fullName); // li hua

employee.fullName = "";
console.log(employee.fullName); // lihua

employee.fullName = "han meimei";
console.log(employee.fullName); // han meimei

如果一个属性存取器只存在gettter,不存在setter,则该存取器会被推断为readonly

静态属性和静态方法

ts支持使用static关键字声明静态属性或者静态方法。

  • 静态成员为所有实例公有,所以内部不能使用this

  • 静态成员不需要实例化就可访问,且实例不能直接通过this访问静态成员

class Grid {
  static origin = { x: 0, y: 0 };
  static calculateDistanceFromOrigin(point: { x: number; y: number }) {
    const xDist = point.x - Grid.origin.x;
    const yDist = point.y - Grid.origin.y;
    return Math.sqrt(xDist * xDist + yDist * yDist);
  }
}

console.log(Grid.calculateDistanceFromOrigin({ x: 10, y: 10 }));

const grid = new Grid();

// Property 'origin' is a static member of type 'Grid'
grid.origin;

抽象类

抽象类在面向对象的语言中也是一个常见的类型,抽象类不能实例化,而继承它的子类(非抽象类)可以实例化,在ts中使用abstract关键字表示抽象类和抽象方法:

abstract class Person {
  constructor(public name: string) {
    this.name = name;
  }
  abstract introduce(): void;
}

抽象类不一定包含抽象方法,但是抽象方法必须在一个抽象类中;并且抽象方法不能有具体实现,只能定义函数声明。

继承抽象类的子类,必须实现父类中的抽象方法:

class Teacher extends Person {
  constructor(name: string) {
    super(name);
  }

  introduce(): void {
    console.log(`my name is ${this.name}, I'm a teacher`);
  }
}

const t = new Teacher("teacher");
t.introduce();

类的类型

当我们声明一个类后,我们既可以把它当做变量来使用,也可以当做类型使用(和枚举很像):

class C {}
const c: C  // C是一个类型
				= new C(); // C是一个变量

实际上,当我们声明一个类时,等价于同时声明了两个类型:

  • 一个类型代表类的实例(使用类作为类型时)
  • 一个类型代表类的构造器以及静态成员(使用类作为变量时)
class Animal {
  static seed: string;
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

// 等价于
interface AnimalInstance {
  name: string;
  getName(): string;
}

interface AnimalConstructor {
  new (name: string): AnimalInstance;
  seed: string;
}


let animal: Animal // 等价于 let animal: AnimalInstance
new Animal(); // 此时Animal的类型是 AnimalConstructor