TS文档学习 --- class(上)

181 阅读7分钟

本篇翻译整理自 TypeScript Handbook 中 「Classes」 章节。

类(Classes)

TypeScript 完全支持 ES2015 引入的 class 关键字。

类成员

这是一个最基本的类,一个空类

class Point {}

字段(Fields)

一个字段声明会创建一个公共(public)可写入(writeable)的属性

class Point {
  // 类型注解是可选的,如果没有指定,会隐式的设置为 any
  x: number;
  y: number;
}
 
const pt = new Point();
pt.x = 0;
pt.y = 0;

字段可以设置初始值(initializers)

class Point {
  // 就像 const 、let 和 var ,一个类属性的初始值会被用于推断它的类型
  x = 0;
  y = 0;
}
 
const pt = new Point();
console.log(`${pt.x}, ${pt.y}`); // => 0, 0

strictPropertyInitialization

strictPropertyInitialization 选项控制了类字段是否需要在构造函数里初始化

class BadGreeter {
  name: string;
  // Property 'name' has no initializer and is not definitely assigned in the constructor.
}
class GoodGreeter {
  name: string;
 
  constructor() {
    this.name = "hello";
  }
}

注意,字段需要在构造函数自身进行初始化。TypeScript 并不会分析构造函数里你调用的方法,进而判断初始化的值,因为一个派生类也许会覆盖这些方法并且初始化成员失败:

class BadGreeter {
  name: string;
  // Property 'name' has no initializer and is not definitely assigned in the constructor.
  setName(): void {
    this.name = '123'
  }
  constructor() {
    this.setName();
  }
}

如果你执意要通过其他方式初始化一个字段,而不是在构造函数里,

你可以使用明确赋值断言操作符(definite assignment assertion operator) !:

class OKGreeter {
  // Not initialized, but no error
  name!: string;
}

readonly

字段可以添加一个 readonly 前缀修饰符,这会阻止在构造函数之外的赋值。

class Greeter {
  readonly name: string = "world";
 
  constructor(otherName?: string) {
    if (otherName !== undefined) {
      this.name = otherName;
    }
  }
 
  err() {
    this.name = "not ok";
		// Cannot assign to 'name' because it is a read-only property.
  }
}

const g = new Greeter();
g.name = "also not ok";
// Cannot assign to 'name' because it is a read-only property.

但typescript并不会阻止你通过构造函数的形式来为name属性进行赋值

可以认为值world其实就是只读属性name的一种特殊的默认值

class Greeter {
  readonly name: string = "world";
 
  constructor(otherName?: string) {
    if (otherName !== undefined) {
      this.name = otherName;
    }
  }
}

const g = new Greeter('Hello TypeScript');

console.log(g.name);

构造函数(Constructors)

类的构造函数跟函数非常类似,你可以使用带类型注解的参数、默认值、重载等。

class Point {
  x: number;
  y: number;
 
  // Normal signature with defaults
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
}
class Point {
  // Overloads
  constructor(x: number, y: string);
  constructor(s: string);
  constructor(xs: any, y?: any) {
    // TBD
  }
}

但类构造函数签名与函数签名之间也有一些区别:

  • 构造函数不能有泛型的类型参数
  • 构造函数不能有返回类型注解,因为总是返回类实例类型

Super 调用

就像在 JavaScript 中,如果你有一个基类,你需要在使用任何 this. 成员之前,先在构造函数里调用 super()

忘记调用 super 是 JavaScript 中一个简单的错误,但是 TypeScript 会在需要的时候提醒你。

方法(Methods)

类中的函数属性被称为方法。方法跟函数、构造函数一样,使用相同的类型注解。

除了标准的类型注解,TypeScript 并没有给方法添加任何新的东西。

class Point {
  x = 10;
  y = 10;
 
  scale(n: number): void {
    this.x *= n;
    this.y *= n;
  }
}

和JavaScript一样,在一个方法体内,它依然可以通过 this. 访问字段和其他的方法。

方法体内一个未限定的名称(unqualified name,没有明确限定作用域的名称)总是指向闭包作用域里的内容

let x: number = 0;
 
class C {
  x: string = "hello";
 
  m() {
    console.log(x); // => 0
    console.log(this.x); // => hello
  }
}

const c = new C();
c.m();

Getters / Setter

和JavaScript一样,类也可以有存取器(accessors)

class C {
  _length = 0;
  get length() {
    return this._length;
  }
  set length(value) {
    this._length = value;
  }
}

TypeScript 对存取器有一些特殊的推断规则:

  • 如果 get 存在而 set 不存在,属性会被自动设置为 readonly
  • 如果 setter 参数的类型没有指定,它会被推断为 getter 的返回类型
  • getters 和 setters 必须有相同的成员可见性(Member Visibility

从 TypeScript 4.3 起,存取器在读取和设置的时候可以使用不同的类型。

class Thing {
  _size = 0;
 
  // 注意这里返回的是 number 类型
  get size(): number {
    return this._size;
  }
 
  // 注意这里允许传入的是 string | number | boolean 类型
  // 虽然参数可以是任意类型,但是赋值给_size的时候,其类型依旧需要是number类型
  set size(value: string | number | boolean) {
    let num = Number(value);
 
    // Don't allow NaN, Infinity, etc
    if (!Number.isFinite(num)) {
      this._size = 0;
      return;
    }
 
    this._size = num;
  }
}

类继承(Class Heritage)

implements 语句

你可以使用 implements 语句检查一个类是否满足一个特定的 interface。如果一个类没有正确的实现(implement)它,TypeScript 会报错:

interface Pingable {
  ping(): void;
}
 
class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}

类也可以实现多个接口,比如 class C implements A, B

注意

implements 语句仅仅检查类是否按照接口类型实现,但它并不会改变类的类型或者方法的类型。

一个常见的错误就是以为 implements 语句会改变类的类型——然而实际上它并不会:

interface Checkable {
  check(name: string): boolean;
}

// implements只是要求我们按照check(name: string): boolean的形式定义check函数
// 但是并不会影响NameChecker中的check函数的类型推断
class NameChecker implements Checkable {
  check(s) {
 		// Parameter 's' implicitly has an 'any' type.
    // Notice no error here
    return s.toLowercse() === "ok";
    				// any
}
interface A {
  x: number;
  y?: number;
}

class C implements A {
  x = 0;
}

const c = new C();
c.y = 10;

// Property 'y' does not exist on type 'C'.

extends 语句

类可以 extend 一个基类。一个派生类有基类所有的属性和方法,还可以定义额外的成员。

class Animal {
  move() {
    console.log("Moving along!");
  }
}
 
class Dog extends Animal {
  woof(times: number) {
    for (let i = 0; i < times; i++) {
      console.log("woof!");
    }
  }
}
 
const d = new Dog();
// Base class method
d.move();
// Derived class method
d.woof(3);

覆写属性

一个派生类可以覆写一个基类的字段或属性。你可以使用 super 语法访问基类的方法

class Base {
  greet() {
    console.log("Hello, world!");
  }
}
 
class Derived extends Base {
  greet(name?: string) {
    if (name === undefined) {
      super.greet();
    } else {
      console.log(`Hello, ${name.toUpperCase()}`);
    }
  }
}
 
const d = new Derived();
d.greet();
d.greet("reader");

派生类需要遵循着它的基类的实现,也就是说派生类必须要兼容它的基类中对应方法的实现。

这是因为我们可以通过一个基类引用指向一个派生类实例

const d = new Derived();

// Alias the derived instance through a base class reference
const b: Base = d;

// 虽然b的类型是Base,但是存储的是Derived的实例,所以这里调用的greet方法,本质上执行的是Derived上的greet方法,而不是Base上的greet方法
b.greet();

但是如果 Derived 不遵循 Base 的约定实现呢?

class Base {
  greet() {
    console.log("Hello, world!");
  }
}
 
class Derived extends Base {
  // Make this parameter required
  greet(name: string) {
	// Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'.
  // Type '(name: string) => void' is not assignable to type '() => void'.
    console.log(`Hello, ${name.toUpperCase()}`);
  }
}

初始化顺序

类初始化的顺序,就像在 JavaScript 中定义的那样:

  • 基类字段初始化
  • 基类构造函数运行
  • 派生类字段初始化
  • 派生类构造函数运行
class Base {
  name = "base";
  constructor() {
    console.log("My name is " + this.name);
  }
}
 
class Derived extends Base {
  name = "derived";

   constructor() {
     super();
    console.log("My name is " + this.name);
  }
}
 
const d = new Derived();
// => "My name is base" 
// => "My name is derived" 

成员可见性

你可以使用 TypeScript 控制某个方法或者属性是否对类以外的代码可见。

public

类成员默认的可见性为 public,一个 public 的成员可以在任何地方被获取

public 是默认的可见性修饰符,所以你不需要写它

class Greeter {
  public greet() {
    console.log("hi!");
  }
}
const g = new Greeter();
g.greet();

protected

protected 成员仅仅对子类可见

class Greeter {
  public greet() {
    console.log("Hello, " + this.getName());
  }
  protected getName() {
    return "hi";
  }
}
 
class SpecialGreeter extends Greeter {
  public howdy() {
    // OK to access protected member here
    console.log("Howdy, " + this.getName());
  }
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName();
// Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.

受保护成员的公开

派生类需要遵循基类的实现,但是依然可以选择公开拥有更多能力的基类子类型

这就包括让一个 protected 成员变成 public

class Base {
  protected m = 10;
}

class Derived extends Base {
  // No modifier, so default is 'public'
  m = 15;
}

const d = new Derived();
console.log(d.m); // => 15

交叉等级受保护成员访问

在TypeScript中,只能在父类自身和子类使用受保护类型成员

不可以在子类中,通过基类引用去获取父类的 protected 成员

class Base {
  protected x: number = 1;

  f2(other: Base) {
    other.x = 10;
  }
}

class Derived1 extends Base {
  protected x: number = 5;

  f1(other: Derived1) {
    other.x = 10;
  }
  
  f2(other: Base) {
    other.x = 10; // error
  }
}

private

private 有点像 protected ,但是不允许访问成员,即便是子类

class Base {
  private x = 0;
}

const b = new Base();

// Can't access from outside the class
console.log(b.x);
// Property 'x' is private and only accessible within class 'Base'.

class Derived extends Base {
  showX() {
    // Can't access in subclasses
    console.log(this.x);
		// Property 'x' is private and only accessible within class 'Base'.
  }
}

因为 private 成员对派生类并不可见,所以一个派生类也不能增加它的可见性

class Base {
  private x = 0;
}

class Derived extends Base {
// Class 'Derived' incorrectly extends base class 'Base'.
// Property 'x' is private in type 'Base' but not in type 'Derived'.
  x = 1;
}

交叉实例私有成员访问

TypeScript 允许一个类的不同实例可以获取彼此的 private 成员

class A {
  private x = 10;
 
  public sameAs(other: A) {
    // No error
    return other.x === this.x;
  }
}

tips

  1. privateprotected 仅仅在类型检查的时候才会强制生效, 并不会影响运行时代码
  2. private是弱私有(soft private)的, 不是严格的强制私有。 这意味着在类型检查的时候,通过方括号语法进行访问
class MySafe {
  private secretKey = 12345;
}
 
const s = new MySafe();
 
// Not allowed during type checking
console.log(s.secretKey);
// Property 'secretKey' is private and only accessible within class 'MySafe'.
 
// OK
console.log(s["secretKey"]);

如果需要一个类的属性是真正的私有化,可以使用JavaScript 的私有字段#

即便是编译后依然保留私有性,并且不会提供像上面这种方括号获取的方法,这让它们变得强私有(hard private)